From b7c393a9fee4a503354041b962078a3609ec0c36 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Thu, 17 May 2012 11:26:19 -0700 Subject: [PATCH 001/239] Add a reactive userId on LivedataConnection This is preliminary work towards an auth system --- packages/livedata/livedata_connection.js | 27 +++++++++++++++++++ .../livedata/livedata_connection_tests.js | 9 +++++++ 2 files changed, 36 insertions(+) diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index aa213fad59..6eaa6f98db 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -374,6 +374,33 @@ _.extend(Meteor._LivedataConnection.prototype, { return self.stream.reconnect(); }, + /// + /// Reactive user system + /// XXX Can/should this be generalized pattern? + /// + userId: function () { + var self = this; + var context = Meteor.deps && Meteor.deps.Context.current; + if (context && !(context.id in self._userIdListeners)) { + self._userIdListeners[context.id] = context; + context.on_invalidate(function () { + delete self._userIdListeners[context.id]; + }); + } + return self._userId; + }, + + setUserId: function (userId) { + var self = this; + self._userId = userId; + _.each(self._userIdListeners, function (context) { + context.invalidate(); + }); + }, + + _userId: null, + _userIdListeners: {}, // context.id -> context + // PRIVATE: called when we are up-to-date with the server. intended // for use only in tests. currently, you are very limited in what // you may do inside your callback -- in particular, don't do diff --git a/packages/livedata/livedata_connection_tests.js b/packages/livedata/livedata_connection_tests.js index b152ebe79c..f0af963b56 100644 --- a/packages/livedata/livedata_connection_tests.js +++ b/packages/livedata/livedata_connection_tests.js @@ -397,6 +397,15 @@ Tinytest.add("livedata stub - reconnect", function (test) { handle.stop(); }); +Tinytest.add("livedata connection - reactive userId", function (test) { + var stream = new Meteor._StubStream(); + var conn = new Meteor._LivedataConnection(stream); + + test.equal(conn.userId(), null); + conn.setUserId(1337); + test.equal(conn.userId(), 1337); +}); + // XXX also test: // - reconnect, with session resume. // - restart on update flag From 55b6bb0deaf965b7fed9760ced8b7ed2f2cd62f5 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 23 May 2012 10:42:15 -0700 Subject: [PATCH 002/239] Method calls can now be forced to wait Meteor.apply and Meteor._LivedataConnection.apply now receive an options parameter which can be used to set the `wait` flag: (Client only) If true, don't send any subsequent method calls until this one is completed. Only run the callback for this method once all previous method calls have completed. --- docs/client/api.html | 2 +- docs/client/api.js | 10 +- packages/livedata/livedata_common.js | 11 +- packages/livedata/livedata_connection.js | 196 ++++++++++++------ .../livedata/livedata_connection_tests.js | 168 +++++++++++---- packages/livedata/livedata_server.js | 12 +- packages/livedata/livedata_test_service.js | 30 +++ packages/livedata/livedata_tests.js | 20 ++ packages/meteor/dynamics_browser.js | 3 + 9 files changed, 342 insertions(+), 110 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index 2598005db8..06566cac32 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -1157,7 +1157,7 @@ Matches a particular type of event, such as 'click'. {{#dtdd "eventtype selector"}} Matches a particular type of event, but only when it appears on -an element that matches a certain CSS selector. +an element that matches a certain CSS selector. {{/dtdd}} {{#dtdd "event1, event2"}} diff --git a/docs/client/api.js b/docs/client/api.js index 9ec88912d5..7ad88ec537 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -209,7 +209,7 @@ Template.api.meteor_call = { Template.api.meteor_apply = { id: "meteor_apply", - name: "Meteor.apply(name, params [, asyncCallback])", + name: "Meteor.apply(name, params [, options] [, asyncCallback])", locus: "Anywhere", descr: ["Invoke a method passing an array of arguments."], args: [ @@ -222,6 +222,12 @@ Template.api.meteor_apply = { {name: "asyncCallback", type: "Function", descr: "Optional callback. If passed, the method runs asynchronously, instead of synchronously, and calls asyncCallback passing either the error or the result."} + ], + options: [ + {name: "wait", + type: "Boolean", + descr: "(Client only) If true, don't send any subsequent method calls until this one is completed. " + + "Only run the callback for this method once all previous method calls have been completed."} ] }; @@ -755,7 +761,7 @@ Template.api.equals = { Template.api.httpcall = { id: "meteor_http_call", - name: "Meteor.http.call(method, url, [options], [asyncCallback])", + name: "Meteor.http.call(method, url [, options] [, asyncCallback])", locus: "Anywhere", descr: ["Perform an outbound HTTP request."], args: [ diff --git a/packages/livedata/livedata_common.js b/packages/livedata/livedata_common.js index 0cc79d32f7..9aba656e1d 100644 --- a/packages/livedata/livedata_common.js +++ b/packages/livedata/livedata_common.js @@ -4,11 +4,12 @@ Meteor._MethodInvocation = function (is_simulation, unblock) { var self = this; // true if we're running not the actual method, but a stub (that is, - // if we're on the client and presently running a simulation of a - // server-side method for latency compensation purposes). never true - // except in a client such as a browser, since there's no point in - // running stubs unless you have a zero-latency connection to the - // user. + // if we're on a client (which may be a browser, or in the future a + // server connecting to another server) and presently running a + // simulation of a server-side method for latency compensation + // purposes). not current true except in a client such as a browser, + // since there's usually no point in running stubs unless you have a + // zero-latency connection to the user. this.is_simulation = is_simulation; // call this function to allow other method invocations (from the diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index 6eaa6f98db..ba4f994b9b 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -31,6 +31,13 @@ Meteor._LivedataConnection = function (url, restart_on_update) { self.next_method_id = 1; // waiting for results of method self.outstanding_methods = []; // each item has keys: msg, callback + // the sole outstanding method that needs to be waited on, or null + self.outstanding_wait_method = null; // same keys as outstanding_methods + // stores response from `outstanding_wait_method` while we wait for + // previous method calls to complete + self.outstanding_wait_method_response = null; + // methods blocked on outstanding_wait_method being completed. + self.blocked_methods = []; // each item has keys: msg, callback, wait // waiting for data from method self.unsatisfied_methods = {}; // map from method_id -> true // sub was ready, is no longer (due to reconnect) @@ -51,31 +58,16 @@ Meteor._LivedataConnection = function (url, restart_on_update) { // just for testing self.quiesce_callbacks = []; - - // Setup auto-reload persistence. var reload_key = "Server-" + url; - var reload_data = Meteor._reload.migration_data(reload_key); - if (typeof reload_data === "object") { - if (typeof reload_data.next_method_id === "number") - self.next_method_id = reload_data.next_method_id; - if (typeof reload_data.outstanding_methods === "object") - self.outstanding_methods = reload_data.outstanding_methods; - // pending messages will be transmitted on initial stream 'reset' - } Meteor._reload.on_migrate(reload_key, function (retry) { if (!self._readyToMigrate()) { if (self.retry_migrate) throw new Error("Two migrations in progress?"); self.retry_migrate = retry; return false; + } else { + return [true]; } - - var methods = _.map(self.outstanding_methods, function (m) { - return {msg: m.msg}; - }); - - return [true, {next_method_id: self.next_method_id, - outstanding_methods: methods}]; }); // Setup stream (if not overriden above) @@ -129,10 +121,7 @@ Meteor._LivedataConnection = function (url, restart_on_update) { // immediately before disconnection.. do we need to add app-level // acking of data messages? - // Send pending methods. - _.each(self.outstanding_methods, function (m) { - self.stream.send(JSON.stringify(m.msg)); - }); + self._sendOutstandingMessages(); // add new subscriptions at the end. this way they take effect after // the handlers and we don't see flicker. @@ -255,11 +244,23 @@ _.extend(Meteor._LivedataConnection.prototype, { return this.apply(name, args, callback); }, - apply: function (name, args, callback) { + // @param options {Optional Object} + // wait: Boolean - Should we block subsequent method calls on this + // method's result having been received? + // (does not affect methods called from within this method) + // @param callback {Optional Function} + apply: function (name, args, options, callback) { var self = this; - var enclosing = Meteor._CurrentInvocation.get(); - if (callback) + // We were passed 3 arguments. They may be either (name, args, options) + // or (name, args, callback) + if (!callback && typeof options === 'function') { + callback = options; + options = {}; + } + options = options || {}; + + if (callback) { // XXX would it be better form to do the binding in stream.on, // or caller, instead of here? callback = Meteor.bindEnvironment(callback, function (e) { @@ -267,8 +268,8 @@ _.extend(Meteor._LivedataConnection.prototype, { Meteor._debug("Exception while delivering result of invoking '" + name + "'", e.stack); }); + } - var is_simulation = enclosing && enclosing.is_simulation; if (Meteor.is_client) { // If on a client, run the stub, if we have one. The stub is // supposed to make some temporary writes to the database to @@ -296,10 +297,10 @@ _.extend(Meteor._LivedataConnection.prototype, { } // If we're in a simulation, stop and return the result we have, - // rather than going on to do an RPC. This can only happen on - // the client (since we only bother with stubs and simulations - // on the client.) If there was not stub, we'll end up returning - // undefined. + // rather than going on to do an RPC. If there was no stub, + // we'll end up returning undefined. + var enclosing = Meteor._CurrentInvocation.get(); + var is_simulation = enclosing && enclosing.is_simulation; if (is_simulation) { if (callback) { callback(exception, ret); @@ -350,9 +351,30 @@ _.extend(Meteor._LivedataConnection.prototype, { params: args, id: '' + (self.next_method_id++) }; - self.outstanding_methods.push({msg: msg, callback: callback}); + if (self.outstanding_wait_method) { + self.blocked_methods.push({ + msg: msg, + callback: callback, + wait: options.wait + }); + } else { + var method_object = { + msg: msg, + callback: callback + }; + + if (options.wait) + self.outstanding_wait_method = method_object; + else + self.outstanding_methods.push(method_object); + + self.stream.send(JSON.stringify(msg)); + } + + // Even if we are waiting on other method calls mark this method + // as unsatisfied so that the user never ends up seeing + // intermediate versions of the server's datastream self.unsatisfied_methods[msg.id] = true; - self.stream.send(JSON.stringify(msg)); // If we're using the default callback on the server, // synchronously return the result from the remote host. @@ -535,36 +557,73 @@ _.extend(Meteor._LivedataConnection.prototype, { }, _livedata_result: function (msg) { - var self = this; // id, result or error. error has error (code), reason, details + var self = this; // find the outstanding request // should be O(1) in nearly all realistic use cases - for (var i = 0; i < self.outstanding_methods.length; i++) { - var m = self.outstanding_methods[i]; - if (m.msg.id === msg.id) - break; + var m; + if (self.outstanding_wait_method && + self.outstanding_wait_method.msg.id === msg.id) { + m = self.outstanding_wait_method; + self.outstanding_wait_method_response = msg; + } else { + for (var i = 0; i < self.outstanding_methods.length; i++) { + m = self.outstanding_methods[i]; + if (m.msg.id === msg.id) + break; + } + + // remove + self.outstanding_methods.splice(i, 1); } + if (!m) { // XXX write a better error Meteor._debug("Can't interpret method response message"); return; } - // remove - self.outstanding_methods.splice(i, 1); + if (self.outstanding_wait_method) { + // Wait until we have completed all outstanding methods. + if (self.outstanding_methods.length === 0 && + self.outstanding_wait_method_response) { + // Fire necessary outstanding method callbacks, making sure we + // only fire the outstanding wait method after all other outstanding + // methods' callbacks were fired + if (m === self.outstanding_wait_method) { + self._deliverMethodResponse(self.outstanding_wait_method, + self.outstanding_wait_method_response /*(=== msg)*/); + } else { + self._deliverMethodResponse(m, msg); + self._deliverMethodResponse(self.outstanding_wait_method, + self.outstanding_wait_method_response /*(!== msg)*/); + } - // deliver result - if (m.callback) { - // callback will have already been bindEnvironment'd by apply(), - // so no need to catch exceptions - if ('error' in msg) - m.callback(new Meteor.Error(msg.error.error, msg.error.reason, - msg.error.details)); - else - // msg.result may be undefined if the method didn't return a - // value - m.callback(undefined, msg.result); + self.outstanding_wait_method = null; + self.outstanding_wait_method_response = null; + + // Find first blocked method with wait: true + var i; + for (i = 0; i < self.blocked_methods.length; i++) + if (self.blocked_methods[i].wait) + break; + + // Move as many blocked methods as we can into outstanding_methods + // and outstanding_wait_method if needed + self.outstanding_methods = _.first(self.blocked_methods, i); + if (i !== self.blocked_methods.length) { + self.outstanding_wait_method = self.blocked_methods[i]; + self.blocked_methods = _.rest(self.blocked_methods, i+1); + } + + self._sendOutstandingMessages(); + } else { + if (m !== self.outstanding_wait_method) + self._deliverMethodResponse(m, msg); + } + } else { + self._deliverMethodResponse(m, msg); } // if we were blocking a migration, see if it's now possible to @@ -575,21 +634,42 @@ _.extend(Meteor._LivedataConnection.prototype, { } }, + // @param method {Object} as in `outstanding_methods` + // @param response {Object{id, result | error}} + _deliverMethodResponse: function(method, response) { + // callback will have already been bindEnvironment'd by apply(), + // so no need to catch exceptions + if ('error' in response) { + method.callback(new Meteor.Error( + response.error.error, response.error.reason, + response.error.details)); + } else { + // msg.result may be undefined if the method didn't return a + // value + method.callback(undefined, response.result); + } + }, + + _sendOutstandingMessages: function() { + var self = this; + _.each(self.outstanding_methods, function (m) { + self.stream.send(JSON.stringify(m.msg)); + }); + if (self.outstanding_wait_method) { + self.stream.send(JSON.stringify(self.outstanding_wait_method.msg)); + } + }, + _livedata_error: function (msg) { Meteor._debug("Received error from server: ", msg.reason); if (msg.offending_message) Meteor._debug("For: ", msg.offending_message); }, - // true if we're OK for a migration to happen - _readyToMigrate: function () { - var self = this; - return _.all(self.outstanding_methods, function (m) { - // Callbacks can't be preserved across migrations, so we can't - // migrate as long as there is an outstanding requests with a - // callback. - return !m.callback; - }); + _readyToMigrate: function() { + return self.outstanding_methods.length === 0 && + !self.outstanding_wait_method && + self.blocking_methods.length === 0; } }); diff --git a/packages/livedata/livedata_connection_tests.js b/packages/livedata/livedata_connection_tests.js index f0af963b56..0ac21bddf5 100644 --- a/packages/livedata/livedata_connection_tests.js +++ b/packages/livedata/livedata_connection_tests.js @@ -12,12 +12,7 @@ var test_got_message = function (test, stream, expected) { test.equal(got, expected); }; -var SESSION_ID = '17'; - -Tinytest.add("livedata stub - receive data", function (test) { - var stream = new Meteor._StubStream(); - var conn = new Meteor._LivedataConnection(stream); - +var startAndConnect = function(test, stream) { stream.reset(); // initial connection start. test_got_message(test, stream, {msg: 'connect'}); @@ -25,6 +20,15 @@ Tinytest.add("livedata stub - receive data", function (test) { stream.receive({msg: 'connected', session: SESSION_ID}); test.length(stream.sent, 0); +}; + +var SESSION_ID = '17'; + +Tinytest.add("livedata stub - receive data", function (test) { + var stream = new Meteor._StubStream(); + var conn = new Meteor._LivedataConnection(stream); + + startAndConnect(test, stream); // data comes in for unknown collection. var coll_name = Meteor.uuid(); @@ -46,19 +50,11 @@ Tinytest.add("livedata stub - receive data", function (test) { test.isUndefined(conn.queued[coll_name]); }); - - Tinytest.add("livedata stub - subscribe", function (test) { var stream = new Meteor._StubStream(); var conn = new Meteor._LivedataConnection(stream); - stream.reset(); // initial connection start. - - test_got_message(test, stream, {msg: 'connect'}); - test.length(stream.sent, 0); - - stream.receive({msg: 'connected', session: SESSION_ID}); - test.length(stream.sent, 0); + startAndConnect(test, stream); // subscribe var callback_fired = false; @@ -82,11 +78,7 @@ Tinytest.add("livedata stub - this", function (test) { var stream = new Meteor._StubStream(); var conn = new Meteor._LivedataConnection(stream); - stream.reset(); // initial connection start. - test_got_message(test, stream, {msg: 'connect'}); - - stream.receive({msg: 'connected', session: SESSION_ID}); - test.length(stream.sent, 0); + startAndConnect(test, stream); conn.methods({test_this: function() { test.isTrue(this.is_simulation); @@ -112,13 +104,7 @@ Tinytest.add("livedata stub - methods", function (test) { var stream = new Meteor._StubStream(); var conn = new Meteor._LivedataConnection(stream); - stream.reset(); // initial connection start. - - test_got_message(test, stream, {msg: 'connect'}); - test.length(stream.sent, 0); - - stream.receive({msg: 'connected', session: SESSION_ID}); - test.length(stream.sent, 0); + startAndConnect(test, stream); var coll_name = Meteor.uuid(); var coll = new Meteor.Collection(coll_name, conn); @@ -211,13 +197,7 @@ Tinytest.add("livedata stub - sub methods", function (test) { var stream = new Meteor._StubStream(); var conn = new Meteor._LivedataConnection(stream); - stream.reset(); // initial connection start. - - test_got_message(test, stream, {msg: 'connect'}); - test.length(stream.sent, 0); - - stream.receive({msg: 'connected', session: SESSION_ID}); - test.length(stream.sent, 0); + startAndConnect(test, stream); var coll_name = Meteor.uuid(); var coll = new Meteor.Collection(coll_name, conn); @@ -287,13 +267,7 @@ Tinytest.add("livedata stub - reconnect", function (test) { var stream = new Meteor._StubStream(); var conn = new Meteor._LivedataConnection(stream); - stream.reset(); // initial connection start. - - test_got_message(test, stream, {msg: 'connect'}); - test.length(stream.sent, 0); - - stream.receive({msg: 'connected', session: SESSION_ID}); - test.length(stream.sent, 0); + startAndConnect(test, stream); var coll_name = Meteor.uuid(); var coll = new Meteor.Collection(coll_name, conn); @@ -342,9 +316,12 @@ Tinytest.add("livedata stub - reconnect", function (test) { conn.call('do_something', function () { method_callback_fired = true; }); + conn.apply('do_something', [], {wait: true}); + test.isFalse(method_callback_fired); var method_message = JSON.parse(stream.sent.shift()); + var wait_method_message = JSON.parse(stream.sent.shift()); test.equal(method_message, {msg: 'method', method: 'do_something', params: [], id:method_message.id}); @@ -354,13 +331,13 @@ Tinytest.add("livedata stub - reconnect", function (test) { test.equal(coll.find({c:3}).count(), 0); test.equal(counts, {added: 1, removed: 0, changed: 1, moved: 0}); - // stream reset. reconnect! // we send a connect, our pending messages, and our subs. stream.reset(); test_got_message(test, stream, {msg: 'connect', session: SESSION_ID}); test_got_message(test, stream, method_message); + test_got_message(test, stream, wait_method_message); test_got_message(test, stream, sub_message); // reconnect with different session id @@ -376,10 +353,12 @@ Tinytest.add("livedata stub - reconnect", function (test) { test.equal(counts, {added: 1, removed: 0, changed: 1, moved: 0}); // satisfy and return method callback - stream.receive({msg: 'data', methods: [method_message.id]}); + stream.receive({msg: 'data', + methods: [method_message.id, wait_method_message.id]}); test.isFalse(method_callback_fired); stream.receive({msg: 'result', id:method_message.id, result:"bupkis"}); + stream.receive({msg: 'result', id:wait_method_message.id, result:"bupkis"}); test.isTrue(method_callback_fired); // still no update. @@ -406,6 +385,109 @@ Tinytest.add("livedata connection - reactive userId", function (test) { test.equal(conn.userId(), 1337); }); +Tinytest.add("livedata connection - two wait methods with reponse in order", function (test) { + var stream = new Meteor._StubStream(); + var conn = new Meteor._LivedataConnection(stream); + startAndConnect(test, stream); + + // setup method + conn.methods({do_something: function (x) {}}); + + var responses = []; + conn.apply('do_something', ['one!'], function() { responses.push('one'); }); + var one_message = JSON.parse(stream.sent.shift()); + test.equal(one_message.params, ['one!']); + + conn.apply('do_something', ['two!'], {wait: true}, function() { + responses.push('two'); + }); + var two_message = JSON.parse(stream.sent.shift()); + test.equal(two_message.params, ['two!']); + test.equal(responses, []); + + conn.apply('do_something', ['three!'], function() { + responses.push('three'); + }); + conn.apply('do_something', ['four!'], {wait: true}, function() { + responses.push('four'); + }); + + conn.apply('do_something', ['five!'], function() { responses.push('five'); }); + + // Verify that we did not send "three!" since we're waiting for + // "one!" and "two!" to send their response back + test.equal(stream.sent.length, 0); + stream.receive({msg: 'result', id: one_message.id}); + test.equal(responses, ['one']); + + test.equal(stream.sent.length, 0); + stream.receive({msg: 'result', id: two_message.id}); + test.equal(responses, ['one', 'two']); + + // Verify that we just sent "three!" and "four!" now that we got + // responses for "one!" and "two!" + test.equal(stream.sent.length, 2); + var three_message = JSON.parse(stream.sent.shift()); + test.equal(three_message.params, ['three!']); + var four_message = JSON.parse(stream.sent.shift()); + test.equal(four_message.params, ['four!']); + + stream.receive({msg: 'result', id: three_message.id}); + test.equal(responses, ['one', 'two', 'three']); + + test.equal(stream.sent.length, 0); + stream.receive({msg: 'result', id: four_message.id}); + test.equal(responses, ['one', 'two', 'three', 'four']); + + // Verify that we just sent "five!" + test.equal(stream.sent.length, 1); + var five_message = JSON.parse(stream.sent.shift()); + test.equal(five_message.params, ['five!']); +}); + +Tinytest.add("livedata connection - one wait method with response out of order", function (test) { + var stream = new Meteor._StubStream(); + var conn = new Meteor._LivedataConnection(stream); + startAndConnect(test, stream); + + // setup method + conn.methods({do_something: function (x) {}}); + + var responses = []; + conn.apply('do_something', ['one!'], function() { responses.push('one'); }); + var one_message = JSON.parse(stream.sent.shift()); + test.equal(one_message.params, ['one!']); + + conn.apply('do_something', ['two!'], {wait: true}, function() { + responses.push('two'); + }); + var two_message = JSON.parse(stream.sent.shift()); + test.equal(two_message.params, ['two!']); + test.equal(responses, []); + + conn.apply('do_something', ['three!']); + + // Verify that we did not send "three!" since we're waiting for + // "one!" and "two!" to send their response back + test.equal(stream.sent.length, 0); + stream.receive({msg: 'result', id: two_message.id}); + test.equal(responses, []); + + test.equal(stream.sent.length, 0); + stream.receive({msg: 'result', id: one_message.id}); + test.equal(responses, ['one', 'two']); // Namely not two, one + + // Verify that we just sent "three!" now that we got responses for + // "one!" and "two!" + test.equal(stream.sent.length, 1); + var three_message = JSON.parse(stream.sent.shift()); + test.equal(three_message.params, ['three!']); + + stream.receive({msg: 'result', id: three_message.id}); + test.equal(stream.sent.length, 0); +}); + + // XXX also test: // - reconnect, with session resume. // - restart on update flag diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js index 4b1744bfc1..f909b361e4 100644 --- a/packages/livedata/livedata_server.js +++ b/packages/livedata/livedata_server.js @@ -755,9 +755,19 @@ _.extend(Meteor._LivedataServer.prototype, { return this.apply(name, args, callback); }, - apply: function (name, args, callback) { + // @param options {Optional Object} + // @param callback {Optional Function} + apply: function (name, args, options, callback) { var self = this; + // We were passed 3 arguments. They may be either (name, args, options) + // or (name, args, callback) + if (!callback && typeof options === 'function') { + callback = options; + options = {}; + } + options = options || {}; + if (callback) // It's not really necessary to do this, since we immediately // run the callback in this fiber before returning, but we do it diff --git a/packages/livedata/livedata_test_service.js b/packages/livedata/livedata_test_service.js index e4f01912d4..6e3485488a 100644 --- a/packages/livedata/livedata_test_service.js +++ b/packages/livedata/livedata_test_service.js @@ -22,6 +22,36 @@ Meteor.methods({ } }); +// Methods to help test applying methods with `wait: true`: delayedTrue +// returns true 500ms after being run unless makeDelayedTrueImmediatelyReturnFalse +// was run in the meanwhile +if (Meteor.is_server) { + var delayed_true_future; + var delayed_true_times; + Meteor.methods({ + delayedTrue: function() { + delayed_true_future = new Future(); + delayed_true_times = Meteor.setTimeout(function() { + delayed_true_future['return'](true); + delayed_true_future = null; + delayed_true_times = null; + }, 500); + + this.unblock(); + return delayed_true_future.wait(); + }, + makeDelayedTrueImmediatelyReturnFalse: function() { + if (!delayed_true_future) + return; // since delayedTrue's timeout had already run + + if (delayed_true_times) clearTimeout(delayed_true_times); + delayed_true_future['return'](false); + delayed_true_future = null; + delayed_true_times = null; + } + }); +} + /*****/ Ledger = new Meteor.Collection("ledger"); diff --git a/packages/livedata/livedata_tests.js b/packages/livedata/livedata_tests.js index 23317cc574..ea8d25089c 100644 --- a/packages/livedata/livedata_tests.js +++ b/packages/livedata/livedata_tests.js @@ -105,6 +105,26 @@ testAsyncMulti("livedata - basic method invocation", [ expect(undefined, [12, {x: 13}])), undefined); }, + // test that `wait: false` is respected + function (test, expect) { + if (Meteor.is_client) { + Meteor.apply("delayedTrue", [], {wait: false}, expect(function(err, res) { + test.equal(res, false); + })); + Meteor.apply("makeDelayedTrueImmediatelyReturnFalse", []); + } + }, + + // test that `wait: true` is respected + function(test, expect) { + if (Meteor.is_client) { + Meteor.apply("delayedTrue", [], {wait: true}, expect(function(err, res) { + test.equal(res, true); + })); + Meteor.apply("makeDelayedTrueImmediatelyReturnFalse", []); + } + }, + function (test, expect) { // No callback diff --git a/packages/meteor/dynamics_browser.js b/packages/meteor/dynamics_browser.js index 9c61ff4e0e..ee85a4d336 100644 --- a/packages/meteor/dynamics_browser.js +++ b/packages/meteor/dynamics_browser.js @@ -27,6 +27,9 @@ }); Meteor.bindEnvironment = function (func, onException, _this) { + // needed in order to be able to create closures inside func and + // have the closed variables not change back to their original + // values var boundValues = _.clone(currentValues); if (!onException) From cc3a163db626343cd466f23c95976c6f103ef42d Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 23 May 2012 10:57:59 -0700 Subject: [PATCH 003/239] Call onReconnect when a LivedataConnection reconnects This can be used to re-establish the proper auth context. --- docs/client/api.html | 3 + docs/client/api.js | 2 +- packages/livedata/livedata_connection.js | 82 ++++++++++++++++--- .../livedata/livedata_connection_tests.js | 76 +++++++++++++++++ 4 files changed, 152 insertions(+), 11 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index 06566cac32..c5912686c3 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -341,6 +341,9 @@ sets, call `Meteor.connect` with the URL of the application. * `apply` * `status` * `reconnect` +* `onReconnect` (this can be set to a function that calls methods as the first + step of reconnecting, for example in order to re-establish the proper auth + context) When you call `Meteor.subscribe`, `Meteor.status`, `Meteor.call`, and `Meteor.apply`, you are using a connection back to that default diff --git a/docs/client/api.js b/docs/client/api.js index 7ad88ec537..40183eaf3a 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -227,7 +227,7 @@ Template.api.meteor_apply = { {name: "wait", type: "Boolean", descr: "(Client only) If true, don't send any subsequent method calls until this one is completed. " - + "Only run the callback for this method once all previous method calls have been completed."} + + "Only run the callback for this method once all previous method calls have completed."} ] }; diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index ba4f994b9b..065b9d6e7d 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -29,15 +29,27 @@ Meteor._LivedataConnection = function (url, restart_on_update) { self.stores = {}; // name -> object with methods self.method_handlers = {}; // name -> func self.next_method_id = 1; - // waiting for results of method + + // --- Three classes of outstanding methods --- + + // 1. either already sent, or waiting to be sent with no special + // consideration once we reconnect self.outstanding_methods = []; // each item has keys: msg, callback - // the sole outstanding method that needs to be waited on, or null + + // 2. the sole outstanding method that needs to be waited on, or null + // same keys as outstanding_methods (notably wait is implicitly true + // but not set) self.outstanding_wait_method = null; // same keys as outstanding_methods // stores response from `outstanding_wait_method` while we wait for - // previous method calls to complete + // previous method calls to complete, as received in _livedata_result self.outstanding_wait_method_response = null; - // methods blocked on outstanding_wait_method being completed. + + // 3. methods blocked on outstanding_wait_method being completed. self.blocked_methods = []; // each item has keys: msg, callback, wait + + // if set, called when we reconnect, queuing method calls _before_ + // the existing outstanding ones + self.onReconnect = null; // waiting for data from method self.unsatisfied_methods = {}; // map from method_id -> true // sub was ready, is no longer (due to reconnect) @@ -121,7 +133,15 @@ Meteor._LivedataConnection = function (url, restart_on_update) { // immediately before disconnection.. do we need to add app-level // acking of data messages? - self._sendOutstandingMessages(); + // If an `onReconnect` handler is set, call it first. Go through + // some hoops to ensure that methods that are called from within + // `onReconnect` get executed _before_ ones that were originally + // outstanding (since `onReconnect` is used to re-establish auth + // certificates) + if (self.onReconnect) + self._callOnReconnectAndSendAppropriateOutstandingMethods(); + else + self._sendOutstandingMethods(); // add new subscriptions at the end. this way they take effect after // the handlers and we don't see flicker. @@ -131,13 +151,14 @@ Meteor._LivedataConnection = function (url, restart_on_update) { }); }); - if (restart_on_update) + if (restart_on_update) { self.stream.on('update_available', function () { // Start trying to migrate to a new version. Until all packages // signal that they're ready for a migration, the app will // continue running normally. Meteor._reload.reload(); }); + } // we never terminate the observe(), since there is no way to // destroy a LivedataConnection.. but this shouldn't matter, since we're @@ -617,7 +638,7 @@ _.extend(Meteor._LivedataConnection.prototype, { self.blocked_methods = _.rest(self.blocked_methods, i+1); } - self._sendOutstandingMessages(); + self._sendOutstandingMethods(); } else { if (m !== self.outstanding_wait_method) self._deliverMethodResponse(m, msg); @@ -650,14 +671,13 @@ _.extend(Meteor._LivedataConnection.prototype, { } }, - _sendOutstandingMessages: function() { + _sendOutstandingMethods: function() { var self = this; _.each(self.outstanding_methods, function (m) { self.stream.send(JSON.stringify(m.msg)); }); - if (self.outstanding_wait_method) { + if (self.outstanding_wait_method) self.stream.send(JSON.stringify(self.outstanding_wait_method.msg)); - } }, _livedata_error: function (msg) { @@ -666,6 +686,48 @@ _.extend(Meteor._LivedataConnection.prototype, { Meteor._debug("For: ", msg.offending_message); }, + _callOnReconnectAndSendAppropriateOutstandingMethods: function() { + var self = this; + var old_outstanding_methods = self.outstanding_methods; + var old_outstanding_wait_method = self.outstanding_wait_method; + var old_blocked_methods = self.blocked_methods; + self.outstanding_methods = []; + self.outstanding_wait_method = null; + self.blocked_methods = []; + + self.onReconnect(); + + if (self.outstanding_wait_method) { + // self.onReconnect() caused us to wait on a method. Add all old + // methods to blocked_methods, and we don't need to send any + // additional methods + self.blocked_methods = self.blocked_methods.concat( + old_outstanding_methods); + + if (old_outstanding_wait_method) { + self.blocked_methods.push(_.extend( + old_outstanding_wait_method, {wait: true})); + } + + self.blocked_methods = self.blocked_methods.concat( + old_blocked_methods); + } else { + // self.onReconnect() did not cause us to wait on a method. Add + // as many methods as we can to outstanding_methods and send + // them + _.each(old_outstanding_methods, function(method) { + self.outstanding_methods.push(method); + self.stream.send(JSON.stringify(method.msg)); + }); + + self.outstanding_wait_method = old_outstanding_wait_method; + if (self.outstanding_wait_method) + self.stream.send(JSON.stringify(self.outstanding_wait_method.msg)); + + self.blocked_methods = old_blocked_methods; + } + }, + _readyToMigrate: function() { return self.outstanding_methods.length === 0 && !self.outstanding_wait_method && diff --git a/packages/livedata/livedata_connection_tests.js b/packages/livedata/livedata_connection_tests.js index 0ac21bddf5..e086bc7a8c 100644 --- a/packages/livedata/livedata_connection_tests.js +++ b/packages/livedata/livedata_connection_tests.js @@ -487,6 +487,82 @@ Tinytest.add("livedata connection - one wait method with response out of order", test.equal(stream.sent.length, 0); }); +Tinytest.add("livedata connection - onReconnect prepends messages correctly with a wait method", function(test) { + var stream = new Meteor._StubStream(); + var conn = new Meteor._LivedataConnection(stream); + startAndConnect(test, stream); + + // setup method + conn.methods({do_something: function (x) {}}); + + conn.onReconnect = function() { + conn.apply('do_something', ['reconnect one']); + conn.apply('do_something', ['reconnect two'], {wait: true}); + conn.apply('do_something', ['reconnect three']); + }; + + conn.apply('do_something', ['one']); + conn.apply('do_something', ['two'], {wait: true}); + conn.apply('do_something', ['three']); + + // reconnect + stream.sent = []; + stream.reset(); + test_got_message( + test, stream, {msg: 'connect', session: conn.last_session_id}); + + // Test that we sent what we expect to send, and we're blocked on + // what we expect to be blocked. The subsequent logic to correctly + // read the wait flag is tested separately. + test.equal(_.map(stream.sent, function(msg) { + return JSON.parse(msg).params[0]; + }), ['reconnect one', 'reconnect two']); + test.equal(_.map(conn.blocked_methods, function(method) { + return [method.msg.params[0], method.wait]; + }), [ + ['reconnect three', undefined/*==false*/], + ['one', undefined/*==false*/], + ['two', true], + ['three', undefined/*==false*/] + ]); +}); + +Tinytest.add("livedata connection - onReconnect prepends messages correctly without a wait method", function(test) { + var stream = new Meteor._StubStream(); + var conn = new Meteor._LivedataConnection(stream); + startAndConnect(test, stream); + + // setup method + conn.methods({do_something: function (x) {}}); + + conn.onReconnect = function() { + conn.apply('do_something', ['reconnect one']); + conn.apply('do_something', ['reconnect two']); + conn.apply('do_something', ['reconnect three']); + }; + + conn.apply('do_something', ['one']); + conn.apply('do_something', ['two'], {wait: true}); + conn.apply('do_something', ['three']); + + // reconnect + stream.sent = []; + stream.reset(); + test_got_message( + test, stream, {msg: 'connect', session: conn.last_session_id}); + + // Test that we sent what we expect to send, and we're blocked on + // what we expect to be blocked. The subsequent logic to correctly + // read the wait flag is tested separately. + test.equal(_.map(stream.sent, function(msg) { + return JSON.parse(msg).params[0]; + }), ['reconnect one', 'reconnect two', 'reconnect three', 'one', 'two']); + test.equal(_.map(conn.blocked_methods, function(method) { + return [method.msg.params[0], method.wait]; + }), [ + ['three', undefined/*==false*/] + ]); +}); // XXX also test: // - reconnect, with session resume. From 414f5e52fbc222b32a26e47ed1bd5dfdaea3ea82 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 23 May 2012 13:22:26 -0700 Subject: [PATCH 004/239] Minor doc changes --- docs/client/api.html | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index c5912686c3..5217bfd632 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -335,16 +335,28 @@ To call methods on another Meteor application or subscribe to its data sets, call `Meteor.connect` with the URL of the application. `Meteor.connect` returns an object which provides: -* `subscribe` -* `methods` (to define stubs) -* `call` -* `apply` -* `status` -* `reconnect` -* `onReconnect` (this can be set to a function that calls methods as the first - step of reconnecting, for example in order to re-establish the proper auth - context) +* `subscribe` - + Subscribe to a record set. See + Meteor.subscribe. +* `call` - + Invoke a method. See Meteor.call. +* `apply` - + Invoke a method with an argument array. See + Meteor.apply. +* `methods` - + Define client-only stubs for methods defined on the remote server. See + Meteor.methods. +* `status` - + Get the current connection status. See + Meteor.status. +* `reconnect` - + See Meteor.reconnect. +* `onReconnect` - Set this to a function to be called as the first step of + reconnecting. This function can call methods which will be executed before + any other outstanding methods. For example, this can be used to re-establish + the appropriate authentication context on the new connection. +By default, clients open a connection to the server from which they're loaded. When you call `Meteor.subscribe`, `Meteor.status`, `Meteor.call`, and `Meteor.apply`, you are using a connection back to that default server. From f1acf247bf29c05f248c5862b26a6dfcea35d2ac Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Thu, 24 May 2012 22:26:40 -0700 Subject: [PATCH 005/239] Initial code necessary to rerun subscriptions on user id changing --- packages/livedata/livedata_server.js | 96 ++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 27 deletions(-) diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js index f909b361e4..d959d86f67 100644 --- a/packages/livedata/livedata_server.js +++ b/packages/livedata/livedata_server.js @@ -30,6 +30,12 @@ Meteor._LivedataSession = function (server) { // map from collection name -> id -> key -> subscription id -> true self.provides_key = {}; + + // if set, ignore flush requests on any subsubcription on this + // session. when set this back to false, don't forget to call flush + // manually. this is sometimes needed because subscriptions + // frequently call flush + self.dontFlush = false; }; _.extend(Meteor._LivedataSession.prototype, { @@ -308,17 +314,23 @@ _.extend(Meteor._LivedataSession.prototype, { else self.universal_subs.push(sub); - var res = handler.apply(sub, params || []); + // Store a function to re-run the handler in case we want to rerun + // subscriptions, for example when the current user id changes + sub._runHandler = function() { + var res = handler.apply(sub, params || []); - // if Meteor._RemoteCollectionDriver is available (defined in - // mongo-livedata), automatically wire up handlers that return a - // Cursor. otherwise, the handler is completely responsible for - // delivering its own data messages and registering stop - // functions. - // - // XXX generalize - if (Meteor._RemoteCollectionDriver && (res instanceof Meteor._Mongo.Cursor)) - sub._publishCursor(res); + // if Meteor._RemoteCollectionDriver is available (defined in + // mongo-livedata), automatically wire up handlers that return a + // Cursor. otherwise, the handler is completely responsible for + // delivering its own data messages and registering stop + // functions. + // + // XXX generalize + if (Meteor._RemoteCollectionDriver && (res instanceof Meteor._Mongo.Cursor)) + sub._publishCursor(res); + }; + + sub._runHandler(); }, // tear down specified subscription @@ -346,7 +358,29 @@ _.extend(Meteor._LivedataSession.prototype, { self.universal_subs = []; }, - // return the current value for a particular key, as given by the + // Rerun all subscriptions without sending intermediate state down + // the wire + _rerunAllSubscriptions: function () { + var self = this; + + var rerunSub = function(sub) { + sub._teardown(); + sub._runHandler(); + }; + var flushSub = function(sub) { + sub.flush(); + }; + + self.dontFlush = true; + _.each(self.named_subs, rerunSub); + _.each(self.universal_subs, rerunSub); + + self.dontFlush = false; + _.each(self.named_subs, flushSub); + _.each(self.universal_subs, flushSub); + }, + + // RETURN the current value for a particular key, as given by the // current contents of each subscription's snapshot. _effectiveValueForKey: function (collection_name, id, key) { var self = this; @@ -410,22 +444,7 @@ _.extend(Meteor._LivedataSubscription.prototype, { if (self.stopped) return; - // tell listeners, so they can clean up - for (var i = 0; i < this.stop_callbacks.length; i++) - (this.stop_callbacks[i])(); - - // remove our data from the client (possibly unshadowing data from - // lower priority subscriptions) - self.pending_data = {}; - self.pending_complete = false; - for (var name in self.snapshot) { - self.pending_data[name] = {}; - for (var id in self.snapshot[name]) { - self.pending_data[name][id] = {}; - for (var key in self.snapshot[name][id]) - self.pending_data[name][id][key] = undefined; - } - } + self._teardown(); self.flush(); self.stopped = true; }, @@ -466,6 +485,9 @@ _.extend(Meteor._LivedataSubscription.prototype, { flush: function () { var self = this; + if (self.session.dontFlush) + return; + if (self.stopped) return; @@ -534,6 +556,26 @@ _.extend(Meteor._LivedataSubscription.prototype, { self.pending_complete = false; }, + _teardown: function() { + var self = this; + // tell listeners, so they can clean up + for (var i = 0; i < self.stop_callbacks.length; i++) + (self.stop_callbacks[i])(); + + // remove our data from the client (possibly unshadowing data from + // lower priority subscriptions) + self.pending_data = {}; + self.pending_complete = false; + for (var name in self.snapshot) { + self.pending_data[name] = {}; + for (var id in self.snapshot[name]) { + self.pending_data[name][id] = {}; + for (var key in self.snapshot[name][id]) + self.pending_data[name][id][key] = undefined; + } + } + }, + _publishCursor: function (cursor, name) { var self = this; var collection = name || cursor.collection_name; From 3a26b18a3fd7b0a2cb93b0c5ca1ea7ad937109d2 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Fri, 25 May 2012 20:46:55 -0700 Subject: [PATCH 006/239] Expose userId/setUserId to method calls on the server. When calling setUserId all subscriptions are rerun. --- packages/livedata/livedata_common.js | 23 +++- packages/livedata/livedata_connection.js | 7 +- packages/livedata/livedata_server.js | 43 ++++++- packages/livedata/livedata_test_service.js | 54 ++++++++- packages/livedata/livedata_tests.js | 124 ++++++++++++++++++++- 5 files changed, 243 insertions(+), 8 deletions(-) diff --git a/packages/livedata/livedata_common.js b/packages/livedata/livedata_common.js index 9aba656e1d..564832cc63 100644 --- a/packages/livedata/livedata_common.js +++ b/packages/livedata/livedata_common.js @@ -1,6 +1,7 @@ // XXX namespacing -Meteor._MethodInvocation = function (is_simulation, unblock) { +Meteor._MethodInvocation = function (is_simulation, userId, + globallySetUserId, unblock) { var self = this; // true if we're running not the actual method, but a stub (that is, @@ -16,8 +17,26 @@ Meteor._MethodInvocation = function (is_simulation, unblock) { // same client) to continue running without waiting for this one to // complete. this.unblock = unblock || function () {}; + + // current user id + this._userId = userId; + + // sets current user id in all appropriate server contexts and + // reruns subscriptions + this._setUserId = globallySetUserId || function () {}; }; +_.extend(Meteor._MethodInvocation.prototype, { + userId: function() { + return this._userId; + }, + + setUserId: function(userId) { + this._userId = userId; + this._setUserId(userId); + } +}); + Meteor._CurrentInvocation = new Meteor.EnvironmentVariable; Meteor.Error = function (error, reason, details) { @@ -41,4 +60,4 @@ Meteor.Error = function (error, reason, details) { self.details = details; }; -Meteor.Error.prototype = new Error; \ No newline at end of file +Meteor.Error.prototype = new Error; diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index 065b9d6e7d..394332f02d 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -306,7 +306,11 @@ _.extend(Meteor._LivedataConnection.prototype, { // of the stub as our return value. var stub = self.method_handlers[name]; if (stub) { - var invocation = new Meteor._MethodInvocation(true /* is_simulation */); + var setUserId = function(userId) { + self.setUserId(userId); + }; + var invocation = new Meteor._MethodInvocation( + true /* is_simulation */, self.userId(), setUserId); try { var ret = Meteor._CurrentInvocation.withValue(invocation,function () { return stub.apply(invocation, args); @@ -372,6 +376,7 @@ _.extend(Meteor._LivedataConnection.prototype, { params: args, id: '' + (self.next_method_id++) }; + if (self.outstanding_wait_method) { self.blocked_methods.push({ msg: msg, diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js index d959d86f67..90ecc661ac 100644 --- a/packages/livedata/livedata_server.js +++ b/packages/livedata/livedata_server.js @@ -36,6 +36,8 @@ Meteor._LivedataSession = function (server) { // manually. this is sometimes needed because subscriptions // frequently call flush self.dontFlush = false; + + self.userId = null; }; _.extend(Meteor._LivedataSession.prototype, { @@ -269,8 +271,12 @@ _.extend(Meteor._LivedataSession.prototype, { return; } - var invocation = new Meteor._MethodInvocation(false /* is_simulation */, - unblock); + var setUserId = function(userId) { + self._setUserId(userId); + }; + + var invocation = new Meteor._MethodInvocation( + false /* is_simulation */, self.userId, setUserId, unblock); try { var ret = Meteor._CurrentWriteFence.withValue(fence, function () { @@ -305,6 +311,14 @@ _.extend(Meteor._LivedataSession.prototype, { } }, + // Sets the current user id in all appropriate contexts and reruns + // all subscriptions + _setUserId: function(userId) { + var self = this; + self.userId = userId; + this._rerunAllSubscriptions(); + }, + _startSubscription: function (handler, priority, sub_id, params) { var self = this; @@ -365,6 +379,7 @@ _.extend(Meteor._LivedataSession.prototype, { var rerunSub = function(sub) { sub._teardown(); + sub._userId = self.userId; sub._runHandler(); }; var flushSub = function(sub) { @@ -435,6 +450,8 @@ Meteor._LivedataSubscription = function (session, sub_id, priority) { // stop callbacks to g/c this sub. called w/ zero arguments. this.stop_callbacks = []; + + this._userId = session.userId; }; _.extend(Meteor._LivedataSubscription.prototype, { @@ -556,6 +573,10 @@ _.extend(Meteor._LivedataSubscription.prototype, { self.pending_complete = false; }, + userId: function() { + return this._userId; + }, + _teardown: function() { var self = this; // tell listeners, so they can clean up @@ -825,7 +846,23 @@ _.extend(Meteor._LivedataServer.prototype, { if (!handler) var exception = new Meteor.Error(404, "Method not found"); else { - var invocation = new Meteor._MethodInvocation(false /* is_simulation */); + // If this is a method call from within another method, get the + // user state from the outer method, otherwise don't allow + // setUserId to be called + var userId = null; + var setUserId = function() { + throw new Error("Can't call setUserId on a server initiated method call"); + }; + var currentInvocation = Meteor._CurrentInvocation.get(); + if (currentInvocation) { + userId = currentInvocation.userId(); + setUserId = function(userId) { + currentInvocation.setUserId(userId); + }; + } + + var invocation = new Meteor._MethodInvocation( + false /* is_simulation */, userId, setUserId); try { var ret = Meteor._CurrentInvocation.withValue(invocation, function () { return handler.apply(invocation, args); diff --git a/packages/livedata/livedata_test_service.js b/packages/livedata/livedata_test_service.js index 6e3485488a..8671c56d2e 100644 --- a/packages/livedata/livedata_test_service.js +++ b/packages/livedata/livedata_test_service.js @@ -90,4 +90,56 @@ Meteor.methods({ Ledger.update({_id: to._id}, {$inc: {balance: amount}}); Meteor.refresh({collection: 'ledger', world: world}); } -}); \ No newline at end of file +}); + +/*****/ + +/// Helpers for "livedata - changing userid reruns subscriptions..." + +objectsWithUsers = new Meteor.Collection("objectsWithUsers"); + +if (Meteor.is_server) { + objectsWithUsers.remove({}); + objectsWithUsers.insert({name: "owned by none", ownerUserId: null}); + objectsWithUsers.insert({name: "owned by one - a", ownerUserId: 1}); + objectsWithUsers.insert({name: "owned by one - b", ownerUserId: 1}); + objectsWithUsers.insert({name: "owned by two - a", ownerUserId: 2}); + objectsWithUsers.insert({name: "owned by two - b", ownerUserId: 2}); + objectsWithUsers.insert({name: "owned by two - c", ownerUserId: 2}); + + Meteor.publish("objectsWithUsers", function() { + return objectsWithUsers.find({ownerUserId: this.userId()}); + }); + + userIdWhenStopped = null; + Meteor.publish("recordUserIdOnStop", function() { + var self = this; + self.onStop(function() { + userIdWhenStopped = self.userId(); + }); + }); + + Meteor.methods({ + setUserId: function(userId) { + this.setUserId(userId); + }, + userIdWhenStopped: function() { + return userIdWhenStopped; + } + }); +} + +/*****/ + +/// Helper for "livedata - setUserId fails when called on server" + +if (Meteor.is_server) { + Meteor.startup(function() { + errorThrownWhenCallingSetUserIdDirectlyOnServer = null; + try { + Meteor.call("setUserId", 1000); + } catch (e) { + errorThrownWhenCallingSetUserIdDirectlyOnServer = e; + } + }); +} diff --git a/packages/livedata/livedata_tests.js b/packages/livedata/livedata_tests.js index ea8d25089c..d388ff3111 100644 --- a/packages/livedata/livedata_tests.js +++ b/packages/livedata/livedata_tests.js @@ -17,7 +17,7 @@ var failure = function (test, code, reason) { } } }; -} +}; Tinytest.add("livedata - Meteor.Error", function (test) { var error = new Meteor.Error(123, "kittens", "puppies"); @@ -258,6 +258,128 @@ testAsyncMulti("livedata - compound methods", [ } ]); +// Replaces the LivedataConnection's `_livedata_data` method to push +// incoming messages on a given collection to an array. This can be +// used to verify that the right data is sent on the wire +// +// @param messages {Array} The array to which to append the messages +// @return {Function} A function to call to undo the eavesdropping +var eavesdropOnCollection = function(livedata_connection, + collection_name, messages) { + old_livedata_data = _.bind( + livedata_connection._livedata_data, livedata_connection); + + // Kind of gross since all tests past this one will run with this + // hook set up. That's probably fine since we only check a specific + // collection but still... + // + // Should we consider having a separate connection per Tinytest or + // some similar scheme? + livedata_connection._livedata_data = function(msg) { + if (msg.collection && msg.collection === collection_name) { + messages.push(msg); + } + old_livedata_data(msg); + }; + + return function() { + livedata_connection._livedata_data = old_livedata_data; + }; +}; + +testAsyncMulti("livedata - changing userid reruns subscriptions without flapping data on the wire", [ + function(test, expect) { + if (Meteor.is_client) { + var messages = []; + var undoEavesdrop = eavesdropOnCollection( + Meteor.default_connection, "objectsWithUsers", messages); + + // A helper for testing incoming set and unset messages + // XXX should this be extracted as a general helper together with + // eavesdropOnCollection? + var testSetAndUnset = function(expectation) { + test.equal(_.map(messages, function(msg) { + var result = {}; + if (msg.set) + result.set = msg.set.name; + if (msg.unset) + result.unset = true; + return result; + }), expectation); + messages.length = 0; // clear messages without creating a new object + }; + + Meteor.subscribe("objectsWithUsers", expect(function() { + testSetAndUnset([{set: "owned by none"}]); + test.equal(objectsWithUsers.find().count(), 1); + Meteor.defer(sendFirstSetUserId); + })); + + // Contorted since we need to call expect at the top level of a test + // (see comment at top of async_multi.js) + + var sendFirstSetUserId = expect(function() { + Meteor.apply("setUserId", [1], {wait: true}); + Meteor.default_connection.onQuiesce(afterFirstSetUserId); + }); + + var afterFirstSetUserId = expect(function() { + testSetAndUnset([ + {unset: true}, + {set: "owned by one - a"}, + {set: "owned by one - b"}]); + test.equal(objectsWithUsers.find().count(), 2); + Meteor.defer(sendSecondSetUserId); + }); + + var sendSecondSetUserId = expect(function() { + Meteor.apply("setUserId", [2], {wait: true}); + Meteor.default_connection.onQuiesce(afterSecondSetUserId); + }); + + var afterSecondSetUserId = expect(function() { + testSetAndUnset([ + {unset: true}, + {unset: true}, + {set: "owned by two - a"}, + {set: "owned by two - b"}, + {set: "owned by two - c"}]); + test.equal(objectsWithUsers.find().count(), 3); + Meteor.defer(sendThirdSetUserId); + }); + + var sendThirdSetUserId = expect(function() { + Meteor.apply("setUserId", [2], {wait: true}); + Meteor.default_connection.onQuiesce(afterThirdSetUserId); + }); + + var afterThirdSetUserId = expect(function() { + // Nothing should have been sent since the results of the + // query are the same ("don't flap data on the wire") + testSetAndUnset([]); + test.equal(objectsWithUsers.find().count(), 3); + undoEavesdrop(); + }); + } + }, function(test, expect) { + if (Meteor.is_client) { + Meteor.subscribe("recordUserIdOnStop"); + Meteor.apply("setUserId", [100], {wait: true}, expect(function() {})); + Meteor.apply("setUserId", [101], {wait: true}, expect(function() {})); + Meteor.call("userIdWhenStopped", expect(function(err, result) { + test.equal(result, 100); + })); + } + } +]); + +Tinytest.add("livedata - setUserId fails when called from server", function(test) { + if (Meteor.is_server) { + test.equal(errorThrownWhenCallingSetUserIdDirectlyOnServer.message, + "Can't call setUserId on a server initiated method call"); + } +}); + // XXX some things to test in greater detail: // staying in simulation mode // time warp From 4d257aa545385b8f062dd0348c1972dfde426040 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Sat, 26 May 2012 03:03:32 -0700 Subject: [PATCH 007/239] Improve test for subscriptions on userid change The test service's subscriptions now have some data overlap for different users, verify that we indeed diff the query results correctly. --- packages/livedata/livedata_test_service.js | 15 ++++++++------- packages/livedata/livedata_tests.js | 13 ++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/livedata/livedata_test_service.js b/packages/livedata/livedata_test_service.js index 8671c56d2e..2984d73479 100644 --- a/packages/livedata/livedata_test_service.js +++ b/packages/livedata/livedata_test_service.js @@ -100,15 +100,16 @@ objectsWithUsers = new Meteor.Collection("objectsWithUsers"); if (Meteor.is_server) { objectsWithUsers.remove({}); - objectsWithUsers.insert({name: "owned by none", ownerUserId: null}); - objectsWithUsers.insert({name: "owned by one - a", ownerUserId: 1}); - objectsWithUsers.insert({name: "owned by one - b", ownerUserId: 1}); - objectsWithUsers.insert({name: "owned by two - a", ownerUserId: 2}); - objectsWithUsers.insert({name: "owned by two - b", ownerUserId: 2}); - objectsWithUsers.insert({name: "owned by two - c", ownerUserId: 2}); + objectsWithUsers.insert({name: "owned by none", ownerUserIds: [null]}); + objectsWithUsers.insert({name: "owned by one - a", ownerUserIds: [1]}); + objectsWithUsers.insert({name: "owned by one/two - a", ownerUserIds: [1, 2]}); + objectsWithUsers.insert({name: "owned by one/two - b", ownerUserIds: [1, 2]}); + objectsWithUsers.insert({name: "owned by two - a", ownerUserIds: [2]}); + objectsWithUsers.insert({name: "owned by two - b", ownerUserIds: [2]}); Meteor.publish("objectsWithUsers", function() { - return objectsWithUsers.find({ownerUserId: this.userId()}); + return objectsWithUsers.find({ownerUserIds: this.userId()}, + {fields: {ownerUserIds: 0}}); }); userIdWhenStopped = null; diff --git a/packages/livedata/livedata_tests.js b/packages/livedata/livedata_tests.js index d388ff3111..ee1d8e5d22 100644 --- a/packages/livedata/livedata_tests.js +++ b/packages/livedata/livedata_tests.js @@ -327,8 +327,9 @@ testAsyncMulti("livedata - changing userid reruns subscriptions without flapping testSetAndUnset([ {unset: true}, {set: "owned by one - a"}, - {set: "owned by one - b"}]); - test.equal(objectsWithUsers.find().count(), 2); + {set: "owned by one/two - a"}, + {set: "owned by one/two - b"}]); + test.equal(objectsWithUsers.find().count(), 3); Meteor.defer(sendSecondSetUserId); }); @@ -339,12 +340,10 @@ testAsyncMulti("livedata - changing userid reruns subscriptions without flapping var afterSecondSetUserId = expect(function() { testSetAndUnset([ - {unset: true}, {unset: true}, {set: "owned by two - a"}, - {set: "owned by two - b"}, - {set: "owned by two - c"}]); - test.equal(objectsWithUsers.find().count(), 3); + {set: "owned by two - b"}]); + test.equal(objectsWithUsers.find().count(), 4); Meteor.defer(sendThirdSetUserId); }); @@ -357,7 +356,7 @@ testAsyncMulti("livedata - changing userid reruns subscriptions without flapping // Nothing should have been sent since the results of the // query are the same ("don't flap data on the wire") testSetAndUnset([]); - test.equal(objectsWithUsers.find().count(), 3); + test.equal(objectsWithUsers.find().count(), 4); undoEavesdrop(); }); } From 301a339eef2dbc86707417df79423b4b6c6e1039 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 30 May 2012 10:49:01 -0700 Subject: [PATCH 008/239] Unbreak reloading app when code changes --- packages/livedata/livedata_connection.js | 3 ++- packages/livedata/livedata_connection_tests.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index 394332f02d..20e7b44a41 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -734,9 +734,10 @@ _.extend(Meteor._LivedataConnection.prototype, { }, _readyToMigrate: function() { + var self = this; return self.outstanding_methods.length === 0 && !self.outstanding_wait_method && - self.blocking_methods.length === 0; + self.blocked_methods.length === 0; } }); diff --git a/packages/livedata/livedata_connection_tests.js b/packages/livedata/livedata_connection_tests.js index e086bc7a8c..e06a4d15ed 100644 --- a/packages/livedata/livedata_connection_tests.js +++ b/packages/livedata/livedata_connection_tests.js @@ -568,3 +568,4 @@ Tinytest.add("livedata connection - onReconnect prepends messages correctly with // - reconnect, with session resume. // - restart on update flag // - on_update event +// - reloading when the app changes, including session migration \ No newline at end of file From f8a0d5a95c6d7923804d548be1aa37129d82425d Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 11 Jun 2012 15:45:26 -0700 Subject: [PATCH 009/239] First pass at an account system backed by Facebook using OAuth --- .gitignore | 2 + packages/accounts/accounts_client.js | 43 +++++++ packages/accounts/accounts_common.js | 16 +++ packages/accounts/accounts_server.js | 170 +++++++++++++++++++++++++++ packages/accounts/package.js | 9 ++ 5 files changed, 240 insertions(+) create mode 100644 packages/accounts/accounts_client.js create mode 100644 packages/accounts/accounts_common.js create mode 100644 packages/accounts/accounts_server.js create mode 100644 packages/accounts/package.js diff --git a/.gitignore b/.gitignore index 7bb4edd405..c210b96ee1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /dev_bundle /dev_bundle*.tar.gz /dist +\#*\# +.\#* \ No newline at end of file diff --git a/packages/accounts/accounts_client.js b/packages/accounts/accounts_client.js new file mode 100644 index 0000000000..4921426850 --- /dev/null +++ b/packages/accounts/accounts_client.js @@ -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}); + }; + }); +}; diff --git a/packages/accounts/accounts_common.js b/packages/accounts/accounts_common.js new file mode 100644 index 0000000000..2a2b6dbf95 --- /dev/null +++ b/packages/accounts/accounts_common.js @@ -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; +}; diff --git a/packages/accounts/accounts_server.js b/packages/accounts/accounts_server.js new file mode 100644 index 0000000000..2618a382b9 --- /dev/null +++ b/packages/accounts/accounts_server.js @@ -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 = + ''; + 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); + } + }; +})(); + diff --git a/packages/accounts/package.js b/packages/accounts/package.js new file mode 100644 index 0000000000..07e5564cf8 --- /dev/null +++ b/packages/accounts/package.js @@ -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'); +}); \ No newline at end of file From 10654b997a6989369ead58f210b7a61210cc78df Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 11 Jun 2012 15:47:32 -0700 Subject: [PATCH 010/239] Preliminary support for user accounts in Todos --- examples/todos/.meteor/packages | 1 + examples/todos/client/todos.css | 27 +++++++++++++++++++++++++++ examples/todos/client/todos.html | 24 ++++++++++++++++++++++++ examples/todos/client/todos.js | 27 ++++++++++++++++++++++++++- examples/todos/fb-app.js | 1 + examples/todos/server/fb-secret.js | 1 + examples/todos/server/publish.js | 7 ++++++- packages/accounts/package.js | 2 ++ 8 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 examples/todos/fb-app.js create mode 100644 examples/todos/server/fb-secret.js diff --git a/examples/todos/.meteor/packages b/examples/todos/.meteor/packages index f96ebb32b8..df9943c56c 100644 --- a/examples/todos/.meteor/packages +++ b/examples/todos/.meteor/packages @@ -5,3 +5,4 @@ underscore backbone +accounts diff --git a/examples/todos/client/todos.css b/examples/todos/client/todos.css index 6657d21e6a..3d3cdc53e4 100644 --- a/examples/todos/client/todos.css +++ b/examples/todos/client/todos.css @@ -259,3 +259,30 @@ h3 { width: 80px; } +#fb-login { + cursor: pointer; + margin: 5px 10px 5px 5px; + padding: 2px 7px; + font-size: 80%; + color: white; + + background: #3B5998; + margin-top: 10px; + + border: 1px solid #777; + border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; +} + +.toggle-privacy-wrapper { + float: right; + width: 110px; +} + +.toggle-privacy { + margin-top: 15px; + float: right; + cursor: pointer; +} diff --git a/examples/todos/client/todos.html b/examples/todos/client/todos.html index f390e82c19..fb1b76ff93 100644 --- a/examples/todos/client/todos.html +++ b/examples/todos/client/todos.html @@ -68,6 +68,21 @@
{{text}}
{{/if}} + {{#if loggedIn}} +
+
+ {{#if privateTo}} + + make public + + {{else}} + + make private + + {{/if}} +
+
+ {{/if}}
{{#each tag_objs}}
@@ -96,7 +111,16 @@ {{tag_text}} ({{count}})
{{/each}} +
+ {{> login}} +
+ diff --git a/examples/todos/client/todos.js b/examples/todos/client/todos.js index 1dfb23a220..943c81904f 100644 --- a/examples/todos/client/todos.js +++ b/examples/todos/client/todos.js @@ -190,6 +190,10 @@ Template.todo_item.adding_tag = function () { return Session.equals('editing_addtag', this._id); }; +Template.todo_item.loggedIn = function() { + return Meteor.default_connection.userId() !== null; +}; + Template.todo_item.events = { 'click .check': function () { Todos.update(this._id, {$set: {done: !this.done}}); @@ -220,8 +224,17 @@ Template.todo_item.events = { Meteor.setTimeout(function () { Todos.update({_id: id}, {$pull: {tags: tag}}); }, 300); - } + }, + 'click .make-public': function () { + Todos.update(this._id, {$set: {privateTo: null}}); + }, + + 'click .make-private': function () { + Todos.update(this._id, {$set: { + privateTo: Meteor.default_connection.userId() + }}); + } }; Template.todo_item.events[ okcancel_events('#todo-input') ] = @@ -287,6 +300,18 @@ Template.tag_filter.events = { } }; +////////// Login ////////// + +Template.login.loggedIn = function() { + return Meteor.default_connection.userId() !== null; +}; + +Template.login.events = { + 'click #fb-login': function () { + Meteor.loginWithFacebook(); + } +}; + ////////// Tracking selected list in URL ////////// var TodosRouter = Backbone.Router.extend({ diff --git a/examples/todos/fb-app.js b/examples/todos/fb-app.js new file mode 100644 index 0000000000..c5869e8354 --- /dev/null +++ b/examples/todos/fb-app.js @@ -0,0 +1 @@ +Meteor.setupFacebook(218833638237574, 'http://auth-todos.meteor.com'); \ No newline at end of file diff --git a/examples/todos/server/fb-secret.js b/examples/todos/server/fb-secret.js new file mode 100644 index 0000000000..eaeed61bab --- /dev/null +++ b/examples/todos/server/fb-secret.js @@ -0,0 +1 @@ +Meteor.setupFacebookSecret('a2f682b4d25e20c245d8ee8135cd82e3'); \ No newline at end of file diff --git a/examples/todos/server/publish.js b/examples/todos/server/publish.js index 76c3630ee2..151241cfd1 100644 --- a/examples/todos/server/publish.js +++ b/examples/todos/server/publish.js @@ -16,6 +16,11 @@ Todos = new Meteor.Collection("todos"); // Publish all items for requested list_id. Meteor.publish('todos', function (list_id) { - return Todos.find({list_id: list_id}); + return Todos.find({ + list_id: list_id, + privateTo: { + $in: [null, this.userId()] + } + }); }); diff --git a/packages/accounts/package.js b/packages/accounts/package.js index 07e5564cf8..57d3b17e7d 100644 --- a/packages/accounts/package.js +++ b/packages/accounts/package.js @@ -3,6 +3,8 @@ Package.describe({ }); Package.on_use(function(api) { + api.use('http', 'server'); + api.add_files('accounts_common.js', ['client', 'server']); api.add_files('accounts_server.js', 'server'); api.add_files('accounts_client.js', 'client'); From c00f2e35514c23e4249d7feb8c3f1fbb2c51cf16 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Tue, 12 Jun 2012 12:40:28 -0700 Subject: [PATCH 011/239] Auth/Accounts improvements - Store login token in local storage so that we log in across tabs - Simulate local storage on IE 7 using userData - Expose a {{user}} helper to Handlerbars - Poll the FB popup so that we know when the user closed it (still without the new design around this) - Support logout - Slightly better error handling (but should still get better) - Support non-autopublishing collections, and make loginTokens such a collection --- examples/todos/client/todos.css | 3 +- examples/todos/client/todos.html | 5 +- examples/todos/client/todos.js | 20 ++++--- examples/todos/fb-app.js | 5 +- examples/todos/server/fb-secret.js | 5 +- packages/accounts/accounts_client.js | 60 ++++++++++++++++++- packages/accounts/accounts_common.js | 10 +++- packages/accounts/accounts_server.js | 31 +++++++--- packages/accounts/localstorage_token.js | 43 +++++++++++++ packages/accounts/package.js | 4 +- .../localstorage_polyfill.js | 39 ++++++++++++ .../localstorage_polyfill_tests.js | 9 +++ packages/localstorage-polyfill/package.js | 14 +++++ packages/mongo-livedata/collection.js | 6 +- 14 files changed, 227 insertions(+), 27 deletions(-) create mode 100644 packages/accounts/localstorage_token.js create mode 100644 packages/localstorage-polyfill/localstorage_polyfill.js create mode 100644 packages/localstorage-polyfill/localstorage_polyfill_tests.js create mode 100644 packages/localstorage-polyfill/package.js diff --git a/examples/todos/client/todos.css b/examples/todos/client/todos.css index 3d3cdc53e4..7fff5e43d1 100644 --- a/examples/todos/client/todos.css +++ b/examples/todos/client/todos.css @@ -259,7 +259,8 @@ h3 { width: 80px; } -#fb-login { +/* XXX remove once we have the login-buttons package */ +.fb-login, #logout { cursor: pointer; margin: 5px 10px 5px 5px; padding: 2px 7px; diff --git a/examples/todos/client/todos.html b/examples/todos/client/todos.html index fb1b76ff93..3b7972a485 100644 --- a/examples/todos/client/todos.html +++ b/examples/todos/client/todos.html @@ -118,9 +118,10 @@ diff --git a/examples/todos/client/todos.js b/examples/todos/client/todos.js index 943c81904f..902675a95c 100644 --- a/examples/todos/client/todos.js +++ b/examples/todos/client/todos.js @@ -19,7 +19,6 @@ Session.set('editing_listname', null); // When editing todo text, ID of the todo Session.set('editing_itemname', null); - // Subscribe to 'lists' collection on startup. // Select a list once data has arrived. Meteor.subscribe('lists', function () { @@ -191,7 +190,7 @@ Template.todo_item.adding_tag = function () { }; Template.todo_item.loggedIn = function() { - return Meteor.default_connection.userId() !== null; + return Meteor.user() !== null; }; Template.todo_item.events = { @@ -232,7 +231,7 @@ Template.todo_item.events = { 'click .make-private': function () { Todos.update(this._id, {$set: { - privateTo: Meteor.default_connection.userId() + privateTo: Meteor.user()._id }}); } }; @@ -302,13 +301,18 @@ Template.tag_filter.events = { ////////// Login ////////// -Template.login.loggedIn = function() { - return Meteor.default_connection.userId() !== null; -}; - Template.login.events = { 'click #fb-login': function () { - Meteor.loginWithFacebook(); + try { + Meteor.loginWithFacebook(); + } catch (e) { + if (e instanceof Meteor.accounts.facebook.SetupError) + alert("You haven't set up your facebook app details. See fb-app.js and server/fb-secret.js"); + } + }, + + 'click #logout': function() { + Meteor.logout(); } }; diff --git a/examples/todos/fb-app.js b/examples/todos/fb-app.js index c5869e8354..f56c515482 100644 --- a/examples/todos/fb-app.js +++ b/examples/todos/fb-app.js @@ -1 +1,4 @@ -Meteor.setupFacebook(218833638237574, 'http://auth-todos.meteor.com'); \ No newline at end of file +// Uncomment and correct following line for integration with Facebook accounts. +// Also see server/fb-secret.js + +// Meteor.accounts.facebook.setup(218833638237574, 'http://auth-todos.meteor.com'); \ No newline at end of file diff --git a/examples/todos/server/fb-secret.js b/examples/todos/server/fb-secret.js index eaeed61bab..d442c37378 100644 --- a/examples/todos/server/fb-secret.js +++ b/examples/todos/server/fb-secret.js @@ -1 +1,4 @@ -Meteor.setupFacebookSecret('a2f682b4d25e20c245d8ee8135cd82e3'); \ No newline at end of file +// Uncomment and correct following line for integration with Facebook accounts. +// Also see ../fb-app.js + +// Meteor.accounts.facebook.setSecret('SECRET'); \ No newline at end of file diff --git a/packages/accounts/accounts_client.js b/packages/accounts/accounts_client.js index 4921426850..615f69a282 100644 --- a/packages/accounts/accounts_client.js +++ b/packages/accounts/accounts_client.js @@ -1,3 +1,18 @@ +Meteor.user = function () { + if (Meteor.default_connection.userId()) { + // XXX full identity? + return {_id: Meteor.default_connection.userId()}; + } else { + return null; + } +}; + +if (Handlebars) { + Handlebars.registerHelper('user', function () { + return Meteor.user(); + }); +} + Meteor.loginWithFacebook = function () { var openCenteredPopup = function(url, width, height) { var screenX = typeof window.screenX !== 'undefined' @@ -19,25 +34,64 @@ Meteor.loginWithFacebook = function () { var newwindow = window.open(url, 'Login', features); if (newwindow.focus) newwindow.focus(); + return newwindow; }; if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl) - throw new Error("Need to call Meteor.accounts.facebook.setup first"); + throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setup first"); var oauthState = Meteor.uuid(); - openCenteredPopup( + var popup = 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? + var checkPopupOpen = setInterval(function() { + if (popup.closed) { + clearInterval(checkPopupOpen); + + // Hit our OAuth endpoint to cancel this login request so that + // the app doesn't hang + Meteor.http.get(Meteor.accounts.facebook._appUrl + + '/_oauth/facebook/?error=error&state=' + oauthState, + function() {}); + } + }, 1000); + Meteor.apply('login', [ {oauth: {version: 2, provider: 'facebook', state: oauthState}} ], {wait: true}, function(error, result) { + if (error) { + Meteor._debug("Server error on login", error); + return; + } + + localStorage.setItem("Meteor.loginToken", result.token); Meteor.default_connection.setUserId(result.id); Meteor.default_connection.onReconnect = function() { - Meteor.apply('login', [{resume: result.token}], {wait: true}); + Meteor.apply('login', [{resume: result.token}], {wait: true}, function(error, result) { + if (error) { + Meteor.default_connection.setUserId(null); + localStorage.setItem("Meteor.loginToken", ""); + Meteor._debug("Server error on login", error); + return; + } + }); }; }); }; + +Meteor.logout = function () { + Meteor.apply('logout', [], {wait: true}, function(error, result) { + if (error) { + Meteor._debug("Server error on logout", error); + return; + } + + localStorage.setItem("Meteor.loginToken", ""); + Meteor.default_connection.setUserId(null); + Meteor.default_connection.onReconnect = null; + }); +}; diff --git a/packages/accounts/accounts_common.js b/packages/accounts/accounts_common.js index 2a2b6dbf95..9a15b34269 100644 --- a/packages/accounts/accounts_common.js +++ b/packages/accounts/accounts_common.js @@ -8,9 +8,17 @@ if (!Meteor.accounts.facebook) { Meteor.accounts.facebook = {}; } -Meteor.accounts._loginTokens = new Meteor.Collection("accounts._loginTokens"); +Meteor.accounts._loginTokens = new Meteor.Collection( + "accounts._loginTokens", + null /*manager*/, + null /*driver*/, + true /*preventAutopublish*/); Meteor.accounts.facebook.setup = function(appId, appUrl) { Meteor.accounts.facebook._appId = appId; Meteor.accounts.facebook._appUrl = appUrl; }; + +Meteor.accounts.facebook.SetupError = function(description) { + this.message = description; +}; diff --git a/packages/accounts/accounts_server.js b/packages/accounts/accounts_server.js index 2618a382b9..534f131cc7 100644 --- a/packages/accounts/accounts_server.js +++ b/packages/accounts/accounts_server.js @@ -24,13 +24,15 @@ .use(function (req, res, next) { Fiber(function() { // Any non-oauth request will continue down the default middlewares - if (req.url.split('/')[1] !== '_oauth') + if (req.url.split('/')[1] !== '_oauth') { next(); + return; + } if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl) - throw new Error("Need to call Meteor.accounts.facebook.setup first"); + throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setup first"); if (!Meteor.accounts.facebook._secret) - throw new Error("Need to call Meteor.accounts.facebook.setSecret first"); + throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setSecret first"); // Close the popup window res.writeHead(200, { 'Content-Type': 'text/html' }); @@ -86,7 +88,7 @@ if (options.oauth) { if (options.oauth.version !== 2 || options.oauth.provider !== 'facebook') - throw new Error("We only support facebook login for now. More soon!"); + throw new Meteor.Error("We only support facebook login for now. More soon!"); var fbAccessToken; if (unmatchedOauthRequests[options.oauth.state]) { @@ -107,6 +109,11 @@ delete oauthFutures[options.oauth.state]; } + if (!fbAccessToken) { + // if cancelled or not authorized + throw new Meteor.Error("Login cancelled or not authorized by user"); + } + // Fetch user's facebook identity var identity = Meteor.http.get( "https://graph.facebook.com/me?access_token=" + fbAccessToken).data; @@ -127,14 +134,17 @@ 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"); + throw new Meteor.Error("Unrecognized options for login request"); } + }, + + logout: function() { + this.setUserId(null); } }); @@ -143,6 +153,13 @@ var bareUrl = req.url.substring(0, req.url.indexOf('?')); var provider = bareUrl.split('/')[2]; if (provider === 'facebook') { + if (req.query.error) { + // Either the user didn't authorize access or we cancelled + // this outstanding login request (such as when the user + // closes the login popup window) + return null; + } + // Request an access token var response = Meteor.http.get( "https://graph.facebook.com/oauth/access_token?" + @@ -163,7 +180,7 @@ return fbAccessToken; } else { - throw new Error("Unknown OAuth provider: " + provider); + throw new Meteor.Error("Unknown OAuth provider: " + provider); } }; })(); diff --git a/packages/accounts/localstorage_token.js b/packages/accounts/localstorage_token.js new file mode 100644 index 0000000000..8d4e891b55 --- /dev/null +++ b/packages/accounts/localstorage_token.js @@ -0,0 +1,43 @@ +// Tries to log in using a meteor token stored in local storage +Meteor.loginFromLocalStorage = function () { + var loginToken = localStorage.getItem("Meteor.loginToken"); + if (loginToken) { + Meteor.apply('login', [{resume: loginToken}], {wait: true}, function(error, result) { + if (error) { + Meteor._debug("Server error on login", error); + return; + } + + Meteor.default_connection.setUserId(result.id); + Meteor.default_connection.onReconnect = function() { + Meteor.apply('login', [{resume: loginToken}], {wait: true}, function(error, result) { + if (error) { + Meteor.default_connection.setUserId(null); + localStorage.setItem("Meteor.loginToken", ""); + Meteor._debug("Server error on login", error); + return; + } + }); + }; + }); + } +}; + +// Immediately try to log in via local storage, so that any DDP +// messages are sent after we have established our user account +Meteor.loginFromLocalStorage(); + +// Poll local storage every 3 seconds to login if someone logged in in +// another tab +Meteor._lastLoginTokenWhenPolled = localStorage.getItem("Meteor.loginToken"); +setInterval(function() { + var currentLoginToken = localStorage.getItem("Meteor.loginToken"); + if (Meteor._lastLoginTokenWhenPolled !== currentLoginToken) { + if (currentLoginToken) + Meteor.loginFromLocalStorage(); + else + Meteor.logout(); + } + Meteor._lastLoginTokenWhenPolled = localStorage.getItem("Meteor.loginToken"); +}, 3000); + diff --git a/packages/accounts/package.js b/packages/accounts/package.js index 57d3b17e7d..24395d3910 100644 --- a/packages/accounts/package.js +++ b/packages/accounts/package.js @@ -3,9 +3,11 @@ Package.describe({ }); Package.on_use(function(api) { - api.use('http', 'server'); + api.use('http', ['client', 'server']); + api.use('localstorage-polyfill', 'client'); api.add_files('accounts_common.js', ['client', 'server']); api.add_files('accounts_server.js', 'server'); api.add_files('accounts_client.js', 'client'); + api.add_files('localstorage_token.js', 'client'); }); \ No newline at end of file diff --git a/packages/localstorage-polyfill/localstorage_polyfill.js b/packages/localstorage-polyfill/localstorage_polyfill.js new file mode 100644 index 0000000000..0cf0881101 --- /dev/null +++ b/packages/localstorage-polyfill/localstorage_polyfill.js @@ -0,0 +1,39 @@ +Meteor.startup(function() { // Since we need document.body to be defined + if (!window.localStorage) { + window.localStorage = (function () { + var userdata = document.createElement('span'); // could be anything + + if (userdata.load) { // If we are on IE, which support userData + userdata.id = 'localstorage-polyfill-helper'; + userdata.style.display = 'none'; + userdata.style.behavior = 'url("#default#userData")'; + document.body.appendChild(userdata); + + var userdataKey = 'localStorage'; + userdata.load(userdataKey); + + return { + setItem: function (key, val) { + userdata.setAttribute(key, val); + userdata.save(userdataKey); + }, + + removeItem: function (key) { + userdata.removeAttribute(key); + userdata.save(userdataKey); + }, + + getItem: function (key) { + return userdata.getAttribute(key); + } + }; + } else { + return { + setItem: function() {}, + removeItem: function() {}, + getItem: function() {} + }; + }; + })(); + } +}); diff --git a/packages/localstorage-polyfill/localstorage_polyfill_tests.js b/packages/localstorage-polyfill/localstorage_polyfill_tests.js new file mode 100644 index 0000000000..af603d54b9 --- /dev/null +++ b/packages/localstorage-polyfill/localstorage_polyfill_tests.js @@ -0,0 +1,9 @@ +Tinytest.add("localStorage polyfill", function (test) { + // Doesn't actually test preservation across reloads since that is hard. + // userData should do that for us so it's unlikely this wouldn't work. + localStorage.setItem("key", "value"); + test.equal(localStorage.getItem("key"), "value"); + localStorage.removeItem("key"); + test.equal(localStorage.getItem("key"), null); +}); + diff --git a/packages/localstorage-polyfill/package.js b/packages/localstorage-polyfill/package.js new file mode 100644 index 0000000000..b065b1fa64 --- /dev/null +++ b/packages/localstorage-polyfill/package.js @@ -0,0 +1,14 @@ +Package.describe({ + summary: "Simulates the localStorage API on IE 6,7 using userData", +}); + +Package.on_use(function (api) { + api.add_files('localstorage_polyfill.js', 'client'); +}); + +Package.on_test(function (api) { + api.use('localstorage-polyfill', 'client'); + api.use('tinytest'); + + api.add_files('localstorage_polyfill_tests.js', 'client'); +}); diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 23e8b3e3bc..cfd8291910 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -1,6 +1,8 @@ // manager, if given, is a LivedataClient or LivedataServer // XXX presently there is no way to destroy/clean up a Collection -Meteor.Collection = function (name, manager, driver) { + +// XXX probably a good idea to change these arguments to be an options map +Meteor.Collection = function (name, manager, driver, preventAutopublish) { var self = this; if (!name && (name !== null)) { @@ -112,7 +114,7 @@ Meteor.Collection = function (name, manager, driver) { } // autopublish - if (manager && manager.onAutopublish) + if (!preventAutopublish && manager && manager.onAutopublish) manager.onAutopublish(function () { var handler = function () { return self.find(); }; manager.publish(null, handler, {is_auto: true}); From 24ca78cb5d808e9771d4cd43fd9c574711ef988c Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 11 Jun 2012 21:09:27 -0700 Subject: [PATCH 012/239] Add callback to be called on success (but not on failure). --- packages/accounts/accounts_client.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/accounts/accounts_client.js b/packages/accounts/accounts_client.js index 615f69a282..8bbb264817 100644 --- a/packages/accounts/accounts_client.js +++ b/packages/accounts/accounts_client.js @@ -13,7 +13,7 @@ if (Handlebars) { }); } -Meteor.loginWithFacebook = function () { +Meteor.loginWithFacebook = function (callback) { var openCenteredPopup = function(url, width, height) { var screenX = typeof window.screenX !== 'undefined' ? window.screenX : window.screenLeft; @@ -80,6 +80,8 @@ Meteor.loginWithFacebook = function () { } }); }; + if (typeof callback === 'function') + callback(); }); }; From d6bc56d2553f5270b089f0337ba0e6190bfc68de Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Tue, 12 Jun 2012 15:08:11 -0700 Subject: [PATCH 013/239] New oauth login flow Also, some code reorganization and bugfix for IE7 --- examples/todos/client/todos.js | 2 + .../unfinished/python-ddp-client/test_input | 12 +- packages/accounts/accounts_client.js | 170 ++++++++---------- packages/accounts/accounts_server.js | 121 +++++++------ packages/accounts/localstorage_token.js | 67 +++++-- packages/accounts/package.js | 3 +- .../localstorage_polyfill.js | 19 +- 7 files changed, 214 insertions(+), 180 deletions(-) diff --git a/examples/todos/client/todos.js b/examples/todos/client/todos.js index 902675a95c..cc0e2c731b 100644 --- a/examples/todos/client/todos.js +++ b/examples/todos/client/todos.js @@ -308,6 +308,8 @@ Template.login.events = { } catch (e) { if (e instanceof Meteor.accounts.facebook.SetupError) alert("You haven't set up your facebook app details. See fb-app.js and server/fb-secret.js"); + else + throw e; } }, diff --git a/examples/unfinished/python-ddp-client/test_input b/examples/unfinished/python-ddp-client/test_input index bdbe747ce8..07da1e3b20 100644 --- a/examples/unfinished/python-ddp-client/test_input +++ b/examples/unfinished/python-ddp-client/test_input @@ -1,15 +1,15 @@ sub -sub xcxc -sub xcxc yzyz -sub xcxc {} +sub undefinedSub +sub undefinedSub someArg +sub undefinedSub {} sub allApps sub myApp "foo.bar" sub myApp ["foo.meteor.com"] call -call xcxc -call xcxc yzyz -call xcxc {} +call undefinedMethod +call undefinedMethod yzyz +call undefinedMethod {} call vote call vote [] call vote ["foo.meteor.com"] diff --git a/packages/accounts/accounts_client.js b/packages/accounts/accounts_client.js index 8bbb264817..ecb3644b9d 100644 --- a/packages/accounts/accounts_client.js +++ b/packages/accounts/accounts_client.js @@ -1,99 +1,87 @@ -Meteor.user = function () { - if (Meteor.default_connection.userId()) { - // XXX full identity? - return {_id: Meteor.default_connection.userId()}; - } else { - return null; - } -}; - -if (Handlebars) { - Handlebars.registerHelper('user', function () { - return Meteor.user(); - }); -} - -Meteor.loginWithFacebook = function (callback) { - 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(); - return newwindow; +(function() { + Meteor.user = function () { + if (Meteor.default_connection.userId()) { + // XXX full identity? + return {_id: Meteor.default_connection.userId()}; + } else { + return null; + } }; - if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl) - throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setup first"); + if (Handlebars) { + Handlebars.registerHelper('user', function () { + return Meteor.user(); + }); + } - var oauthState = Meteor.uuid(); + 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); - var popup = 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? + // 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 checkPopupOpen = setInterval(function() { - if (popup.closed) { - clearInterval(checkPopupOpen); - - // Hit our OAuth endpoint to cancel this login request so that - // the app doesn't hang - Meteor.http.get(Meteor.accounts.facebook._appUrl + - '/_oauth/facebook/?error=error&state=' + oauthState, - function() {}); - } - }, 1000); - - Meteor.apply('login', [ - {oauth: {version: 2, provider: 'facebook', state: oauthState}} - ], {wait: true}, function(error, result) { - if (error) { - Meteor._debug("Server error on login", error); - return; - } - - localStorage.setItem("Meteor.loginToken", result.token); - Meteor.default_connection.setUserId(result.id); - Meteor.default_connection.onReconnect = function() { - Meteor.apply('login', [{resume: result.token}], {wait: true}, function(error, result) { - if (error) { - Meteor.default_connection.setUserId(null); - localStorage.setItem("Meteor.loginToken", ""); - Meteor._debug("Server error on login", error); - return; - } - }); + var newwindow = window.open(url, 'Login', features); + if (newwindow.focus) + newwindow.focus(); + return newwindow; }; - if (typeof callback === 'function') - callback(); - }); -}; -Meteor.logout = function () { - Meteor.apply('logout', [], {wait: true}, function(error, result) { - if (error) { - Meteor._debug("Server error on logout", error); - return; - } + if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl) + throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setup first"); - localStorage.setItem("Meteor.loginToken", ""); - Meteor.default_connection.setUserId(null); - Meteor.default_connection.onReconnect = null; - }); -}; + var oauthState = Meteor.uuid(); + + var popup = openCenteredPopup( + 'https://www.facebook.com/dialog/oauth?client_id=' + Meteor.accounts.facebook._appId + + '&redirect_uri=' + Meteor.accounts.facebook._appUrl + '/_oauth/facebook?close' + + '&scope=email&state=' + oauthState, + 1000, 600); // XXX should we use different dimensions, e.g. on mobile? + + var checkPopupOpen = setInterval(function() { + if (popup.closed) { + clearInterval(checkPopupOpen); + tryLoginAfterPopupClosed(oauthState); + } + }, 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(oauthState) { + Meteor.apply('login', [ + {oauth: {version: 2, provider: 'facebook', state: oauthState}} + ], {wait: true}, function(error, result) { + if (error) { + Meteor._debug("Server error on login", error); + return; + } + + Meteor.accounts.loginAndStoreToken(result.token); + callback && callback(); + }); + }; + + Meteor.logout = function () { + Meteor.apply('logout', [], {wait: true}, function(error, result) { + if (error) { + Meteor._debug("Server error on logout", error); + return; + } else { + Meteor.accounts.forceClientLoggedOut(); + } + }); + }; +})(); diff --git a/packages/accounts/accounts_server.js b/packages/accounts/accounts_server.js index 534f131cc7..81571f4b7e 100644 --- a/packages/accounts/accounts_server.js +++ b/packages/accounts/accounts_server.js @@ -2,17 +2,12 @@ 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 + // Incoming OAuth http requests are recorded here when the OAuth + // process is completed inside a popup window. Afterwards, these are + // read by the OAuth login method to complete the process. + // + // @type {Object} maps from Oauth "state" to request + Meteor.accounts._unmatchedOauthRequests = {}; Meteor.accounts.facebook.setSecret = function(secret) { Meteor.accounts.facebook._secret = secret; @@ -34,25 +29,31 @@ if (!Meteor.accounts.facebook._secret) throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setSecret first"); - // Close the popup window - res.writeHead(200, { 'Content-Type': 'text/html' }); - var content = - ''; - res.end(content, 'utf-8'); + Meteor.accounts._unmatchedOauthRequests[req.query.state] = req; - // Try to unblock the appropriate call to `login` - var future = oauthFutures[req.query.state]; - if (future) { - // Unblock the `login` call - future.return(handleOauthRequest(req)); + // We support /_oauth?close, /_oauth?redirect=URL. Any other /_oauth request + // just served 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'}); + var content = + ''; + res.end(content, 'utf-8'); + } else if (req.query.redirect) { + res.writeHead(302, {'Location': req.query.redirect}); + res.end(); } else { - // Store this request. We expect to soon get a call to `login` - unmatchedOauthRequests[req.query.state] = req; + res.writeHead(200, {'Content-Type': 'text/html'}); + res.end(content, 'utf-8'); } }).run(); }); 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) { // XXX write test for updateOrCreateUser var updateOrCreateUser = function(email, fbId, fbAccessToken) { @@ -91,27 +92,18 @@ throw new Meteor.Error("We only support facebook login for now. More soon!"); var fbAccessToken; - if (unmatchedOauthRequests[options.oauth.state]) { + var unmatchedRequest = Meteor.accounts._unmatchedOauthRequests[options.oauth.state]; + if (unmatchedRequest) { // We had previously received the HTTP request with the OAuth code - fbAccessToken = handleOauthRequest( - unmatchedOauthRequests[options.oauth.state]); - delete unmatchedOauthRequests[options.oauth.state]; + fbAccessToken = handleOauthRequest(unmatchedRequest); + delete Meteor.accounts._unmatchedOauthRequests[options.oauth.state]; + + // If the user didn't authorize the login, either explicitly + // or by closing the popup window, return null + if (!fbAccessToken) + return null; } 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]; - } - - if (!fbAccessToken) { - // if cancelled or not authorized - throw new Meteor.Error("Login cancelled or not authorized by user"); + return null; } // Fetch user's facebook identity @@ -141,10 +133,10 @@ } else { throw new Meteor.Error("Unrecognized options for login request"); } - }, + }, - logout: function() { - this.setUserId(null); + logout: function() { + this.setUserId(null); } }); @@ -164,21 +156,38 @@ 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" + + "&redirect_uri=" + Meteor.accounts.facebook._appUrl + "/_oauth/facebook?close" + "&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? - }); + // Errors come back as JSON but success looks like a query encoded in a url + var error_response = null; + try { + // Just try to parse so that we know if we failed or not, + // while storing the parsed results + var error_response = JSON.parse(response); + } catch (e) { + } - return fbAccessToken; + if (error_response) { + if (error_response.error) { + throw new Meteor.Error("Error trying to get access token from Facebook", error_response); + } else { + throw new Meteor.Error("Unexpected response when trying to get access token from Facebook", error_response); + } + } else { + // Success! 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 Meteor.Error("Unknown OAuth provider: " + provider); } diff --git a/packages/accounts/localstorage_token.js b/packages/accounts/localstorage_token.js index 8d4e891b55..781ab3dbc1 100644 --- a/packages/accounts/localstorage_token.js +++ b/packages/accounts/localstorage_token.js @@ -1,6 +1,31 @@ +(function() { + // To be used as the local storage key + var loginTokenKey = "Meteor.loginToken"; + + Meteor.accounts.loginAndStoreToken = function(token) { + localStorage.setItem(loginTokenKey, token); + Meteor.loginFromLocalStorage(); + }; + + Meteor.accounts.unstoreLoginToken = function() { + localStorage.removeItem(loginTokenKey); + }; + + Meteor.accounts.storedLoginToken = function() { + return localStorage.getItem(loginTokenKey); + }; + + Meteor.accounts.forceClientLoggedOut = function() { + Meteor.accounts.unstoreLoginToken(); + Meteor.default_connection.setUserId(null); + Meteor.default_connection.onReconnect = null; + }; +})(); + // Tries to log in using a meteor token stored in local storage Meteor.loginFromLocalStorage = function () { - var loginToken = localStorage.getItem("Meteor.loginToken"); + var loginToken = Meteor.accounts.storedLoginToken(); + Meteor.accounts._lastLoginTokenWhenPolled = loginToken; if (loginToken) { Meteor.apply('login', [{resume: loginToken}], {wait: true}, function(error, result) { if (error) { @@ -12,8 +37,7 @@ Meteor.loginFromLocalStorage = function () { Meteor.default_connection.onReconnect = function() { Meteor.apply('login', [{resume: loginToken}], {wait: true}, function(error, result) { if (error) { - Meteor.default_connection.setUserId(null); - localStorage.setItem("Meteor.loginToken", ""); + Meteor.accounts.forceClientLoggedOut(); Meteor._debug("Server error on login", error); return; } @@ -23,21 +47,26 @@ Meteor.loginFromLocalStorage = function () { } }; -// Immediately try to log in via local storage, so that any DDP -// messages are sent after we have established our user account -Meteor.loginFromLocalStorage(); +Meteor.startup(function() { + // Immediately try to log in via local storage, so that any DDP + // messages are sent after we have established our user account + // + // NOTE: This must happen in a Meteor.startup block because on IE we + // need to have installed the localStorage polyfill (see package + // `localstorage-polyfill`) + Meteor.loginFromLocalStorage(); -// Poll local storage every 3 seconds to login if someone logged in in -// another tab -Meteor._lastLoginTokenWhenPolled = localStorage.getItem("Meteor.loginToken"); -setInterval(function() { - var currentLoginToken = localStorage.getItem("Meteor.loginToken"); - if (Meteor._lastLoginTokenWhenPolled !== currentLoginToken) { - if (currentLoginToken) - Meteor.loginFromLocalStorage(); - else - Meteor.logout(); - } - Meteor._lastLoginTokenWhenPolled = localStorage.getItem("Meteor.loginToken"); -}, 3000); + // Poll local storage every 3 seconds to login if someone logged in in + // another tab + setInterval(function() { + var currentLoginToken = Meteor.accounts.storedLoginToken(); + if (Meteor.accounts._lastLoginTokenWhenPolled !== currentLoginToken) { + if (currentLoginToken) + Meteor.loginFromLocalStorage(); + else + Meteor.logout(); + } + Meteor._lastLoginTokenWhenPolled = currentLoginToken; + }, 3000); +}); diff --git a/packages/accounts/package.js b/packages/accounts/package.js index 24395d3910..1da6e15120 100644 --- a/packages/accounts/package.js +++ b/packages/accounts/package.js @@ -8,6 +8,7 @@ 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'); + api.add_files('localstorage_token.js', 'client'); + api.add_files('accounts_client.js', 'client'); }); \ No newline at end of file diff --git a/packages/localstorage-polyfill/localstorage_polyfill.js b/packages/localstorage-polyfill/localstorage_polyfill.js index 0cf0881101..d8d473136e 100644 --- a/packages/localstorage-polyfill/localstorage_polyfill.js +++ b/packages/localstorage-polyfill/localstorage_polyfill.js @@ -1,12 +1,11 @@ Meteor.startup(function() { // Since we need document.body to be defined if (!window.localStorage) { window.localStorage = (function () { - var userdata = document.createElement('span'); // could be anything - - if (userdata.load) { // If we are on IE, which support userData + if ($.browser.msie) { // If we are on IE, which support userData + var userdata = document.createElement('span'); // could be anything + userdata.style.behavior = 'url("#default#userData")'; userdata.id = 'localstorage-polyfill-helper'; userdata.style.display = 'none'; - userdata.style.behavior = 'url("#default#userData")'; document.body.appendChild(userdata); var userdataKey = 'localStorage'; @@ -24,14 +23,20 @@ Meteor.startup(function() { // Since we need document.body to be defined }, getItem: function (key) { + userdata.load(userdataKey); return userdata.getAttribute(key); } }; } else { + Meteor._debug( + "You are running a browser with no localStorage or userData " + + "support (presumable Opera Mini). Logging in from one tab " + + "will not cause another tab to be logged in."); + return { - setItem: function() {}, - removeItem: function() {}, - getItem: function() {} + setItem: function () {}, + removeItem: function () {}, + getItem: function () {} }; }; })(); From 2fc45793ee0e345fcd5f850cc6165905623bf793 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Tue, 12 Jun 2012 15:08:11 -0700 Subject: [PATCH 014/239] Support for Google login + refactor of accounts packages Break up the accounts package into accounts, accounts-facebook, oauth2. --- examples/todos/.meteor/packages | 2 + examples/todos/client/todos.html | 3 +- examples/todos/client/todos.js | 13 +- examples/todos/google-api.js | 4 + examples/todos/server/google-secret.js | 4 + packages/accounts-facebook/facebook_client.js | 21 ++ packages/accounts-facebook/facebook_common.js | 12 + packages/accounts-facebook/facebook_server.js | 79 ++++++ packages/accounts-facebook/package.js | 13 + packages/accounts-google/google_client.js | 19 ++ packages/accounts-google/google_common.js | 12 + packages/accounts-google/google_server.js | 45 ++++ packages/accounts-google/package.js | 13 + packages/accounts/accounts_client.js | 70 +----- packages/accounts/accounts_common.js | 13 - packages/accounts/accounts_server.js | 238 ++++++------------ packages/accounts/localstorage_token.js | 11 +- packages/accounts/package.js | 2 +- .../localstorage_polyfill.js | 5 +- packages/oauth2/oauth2_client.js | 56 +++++ packages/oauth2/oauth2_common.js | 1 + packages/oauth2/oauth2_server.js | 71 ++++++ packages/oauth2/package.js | 12 + 23 files changed, 471 insertions(+), 248 deletions(-) create mode 100644 examples/todos/google-api.js create mode 100644 examples/todos/server/google-secret.js create mode 100644 packages/accounts-facebook/facebook_client.js create mode 100644 packages/accounts-facebook/facebook_common.js create mode 100644 packages/accounts-facebook/facebook_server.js create mode 100644 packages/accounts-facebook/package.js create mode 100644 packages/accounts-google/google_client.js create mode 100644 packages/accounts-google/google_common.js create mode 100644 packages/accounts-google/google_server.js create mode 100644 packages/accounts-google/package.js create mode 100644 packages/oauth2/oauth2_client.js create mode 100644 packages/oauth2/oauth2_common.js create mode 100644 packages/oauth2/oauth2_server.js create mode 100644 packages/oauth2/package.js diff --git a/examples/todos/.meteor/packages b/examples/todos/.meteor/packages index df9943c56c..d35f3979f0 100644 --- a/examples/todos/.meteor/packages +++ b/examples/todos/.meteor/packages @@ -6,3 +6,5 @@ underscore backbone accounts +accounts-facebook +accounts-google diff --git a/examples/todos/client/todos.html b/examples/todos/client/todos.html index 3b7972a485..d5456fcd88 100644 --- a/examples/todos/client/todos.html +++ b/examples/todos/client/todos.html @@ -121,7 +121,8 @@ {{#if user}}
logout
{{else}} - +
login using facebook
+
login using google
{{/if}} diff --git a/examples/todos/client/todos.js b/examples/todos/client/todos.js index cc0e2c731b..6f45cb1f71 100644 --- a/examples/todos/client/todos.js +++ b/examples/todos/client/todos.js @@ -307,12 +307,23 @@ Template.login.events = { Meteor.loginWithFacebook(); } catch (e) { if (e instanceof Meteor.accounts.facebook.SetupError) - alert("You haven't set up your facebook app details. See fb-app.js and server/fb-secret.js"); + alert("You haven't set up your Facebook app details. See fb-app.js and server/fb-secret.js"); else throw e; } }, + 'click #google-login': function () { + try { + Meteor.loginWithGoogle(); + } catch (e) { + if (e instanceof Meteor.accounts.google.SetupError) + alert("You haven't set up your Google API details. See google-api.js and server/google-secret.js"); + else + throw e; + }; + }, + 'click #logout': function() { Meteor.logout(); } diff --git a/examples/todos/google-api.js b/examples/todos/google-api.js new file mode 100644 index 0000000000..7639342f02 --- /dev/null +++ b/examples/todos/google-api.js @@ -0,0 +1,4 @@ +// Uncomment and correct following line for integration with Google accounts. +// Also see server/google-secret.js + +// Meteor.accounts.google.setup('987846107089.apps.googleusercontent.com', 'http://auth-todos.meteor.com'); diff --git a/examples/todos/server/google-secret.js b/examples/todos/server/google-secret.js new file mode 100644 index 0000000000..822ca36683 --- /dev/null +++ b/examples/todos/server/google-secret.js @@ -0,0 +1,4 @@ +// Uncomment and correct following line for integration with Google accounts. +// Also see ../google-api.js + +// Meteor.accounts.google.setSecret('SECRET'); \ No newline at end of file diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js new file mode 100644 index 0000000000..60cca7f60f --- /dev/null +++ b/packages/accounts-facebook/facebook_client.js @@ -0,0 +1,21 @@ +(function () { + Meteor.loginWithFacebook = function () { + if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl) + throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setup first"); + + var state = Meteor.uuid(); + // XXX I think there's a smaller popup. Replace with appropriate URL. + // XXX need to support configuring scope + var loginUrl = + 'https://www.facebook.com/dialog/oauth?client_id=' + Meteor.accounts.facebook._appId + + '&redirect_uri=' + Meteor.accounts.facebook._appUrl + '/_oauth/facebook?close' + + '&scope=email&state=' + state; + + Meteor.accounts.oauth2.initiateLogin(state, loginUrl); + }; + +})(); + + + + diff --git a/packages/accounts-facebook/facebook_common.js b/packages/accounts-facebook/facebook_common.js new file mode 100644 index 0000000000..f272f8a89c --- /dev/null +++ b/packages/accounts-facebook/facebook_common.js @@ -0,0 +1,12 @@ +if (!Meteor.accounts.facebook) { + Meteor.accounts.facebook = {}; +} + +Meteor.accounts.facebook.setup = function(appId, appUrl) { + Meteor.accounts.facebook._appId = appId; + Meteor.accounts.facebook._appUrl = appUrl; +}; + +Meteor.accounts.facebook.SetupError = function(description) { + this.message = description; +}; diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js new file mode 100644 index 0000000000..51efee1cb8 --- /dev/null +++ b/packages/accounts-facebook/facebook_server.js @@ -0,0 +1,79 @@ +(function () { + + Meteor.accounts.facebook.setSecret = function (secret) { + Meteor.accounts.facebook._secret = secret; + }; + + // register the facebook identity provider + Meteor.accounts.oauth2.providers.facebook = { + userIdForOauthReq: function(req) { + if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl) + throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setup first"); + if (!Meteor.accounts.facebook._secret) + throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setSecret first"); + + var accessToken = getAccessToken(req); + // If the user didn't authorize the login, either explicitly + // or by closing the popup window, return null + if (!accessToken) + return null; + + // Fetch user's facebook identity + var identity = Meteor.http.get("https://graph.facebook.com/me", { + params: {access_token: accessToken}}).data; + + return Meteor.accounts.updateOrCreateUser( + identity.email, 'facebook', identity.id, + {accessToken: accessToken}); + } + }; + + // @returns {String} Facebook access token + var getAccessToken = function (req) { + if (req.query.error) { + // The user didn't authorize access + return null; + } + + // Request an access token + var response = 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, + code: req.query.code + } + }).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, + // while storing the parsed results + error_response = JSON.parse(response); + } catch (e) { + error_response = null; + } + + if (error_response) { + if (error_response.error) { + throw new Meteor.Error("Error trying to get access token from Facebook", error_response); + } else { + throw new Meteor.Error("Unexpected response when trying to get access token from Facebook", error_response); + } + } else { + // Success! 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; + } + }; +}) (); \ No newline at end of file diff --git a/packages/accounts-facebook/package.js b/packages/accounts-facebook/package.js new file mode 100644 index 0000000000..49ce3d95bd --- /dev/null +++ b/packages/accounts-facebook/package.js @@ -0,0 +1,13 @@ +Package.describe({ + summary: "Integration with facebook accounts", +}); + +Package.on_use(function(api) { + api.use('accounts', ['client', 'server']); + api.use('oauth2', ['client', 'server']); + api.use('http', ['client', 'server']); + + api.add_files('facebook_common.js', ['client', 'server']); + api.add_files('facebook_server.js', 'server'); + api.add_files('facebook_client.js', 'client'); +}); diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js new file mode 100644 index 0000000000..7bb0ed49fa --- /dev/null +++ b/packages/accounts-google/google_client.js @@ -0,0 +1,19 @@ +(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"); + + var state = Meteor.uuid(); + // XXX need to support configuring access_type and scopy + var loginUrl = + 'https://accounts.google.com/o/oauth2/auth' + + '?response_type=code' + + '&client_id=' + Meteor.accounts.google._clientId + + '&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile' + + '&redirect_uri=' + Meteor.accounts.google._appUrl + '/_oauth/google?close' + + '&state=' + state; + + Meteor.accounts.oauth2.initiateLogin(state, loginUrl); + }; + +}) (); diff --git a/packages/accounts-google/google_common.js b/packages/accounts-google/google_common.js new file mode 100644 index 0000000000..18ce2c56fc --- /dev/null +++ b/packages/accounts-google/google_common.js @@ -0,0 +1,12 @@ +if (!Meteor.accounts.google) { + Meteor.accounts.google = {}; +} + +Meteor.accounts.google.setup = function(clientId, appUrl) { + Meteor.accounts.google._clientId = clientId; + Meteor.accounts.google._appUrl = appUrl; +}; + +Meteor.accounts.google.SetupError = function(description) { + this.message = description; +}; diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js new file mode 100644 index 0000000000..af40eb392c --- /dev/null +++ b/packages/accounts-google/google_server.js @@ -0,0 +1,45 @@ +(function () { + Meteor.accounts.google.setSecret = function (secret) { + Meteor.accounts.google._secret = secret; + }; + + Meteor.accounts.oauth2.providers.google = { + userIdForOauthReq: function(req) { + var accessToken = getAccessToken(req); + + // XXX can we generalize this flow into the oauth abstraction? + if (!accessToken) + return null; + + var identity = Meteor.http.get( + "https://www.googleapis.com/oauth2/v1/userinfo", + {params: {access_token: accessToken}}).data; + + return Meteor.accounts.updateOrCreateUser( + identity.email, 'google', identity.id, + {accessToken: accessToken}); + } + }; + + var getAccessToken = function (req) { + if (req.query.error) { + // The user didn't authorize access + // XXX can we generalize this into the oauth abstration? + return null; + } + + var response = Meteor.http.post( + "https://accounts.google.com/o/oauth2/token", {params: { + code: req.query.code, + client_id: Meteor.accounts.google._clientId, + client_secret: Meteor.accounts.google._secret, + redirect_uri: Meteor.accounts.google._appUrl + "/_oauth/google?close", + grant_type: 'authorization_code' + }}).data; + + if (response.error) + throw response; + + return response.access_token; + }; +})(); \ No newline at end of file diff --git a/packages/accounts-google/package.js b/packages/accounts-google/package.js new file mode 100644 index 0000000000..ca4119491d --- /dev/null +++ b/packages/accounts-google/package.js @@ -0,0 +1,13 @@ +Package.describe({ + summary: "Integration with google accounts", +}); + +Package.on_use(function(api) { + api.use('accounts', ['client', 'server']); + api.use('oauth2', ['client', 'server']); + api.use('http', ['client', 'server']); + + api.add_files('google_common.js', ['client', 'server']); + api.add_files('google_server.js', 'server'); + api.add_files('google_client.js', 'client'); +}); diff --git a/packages/accounts/accounts_client.js b/packages/accounts/accounts_client.js index ecb3644b9d..1f93cfa5ac 100644 --- a/packages/accounts/accounts_client.js +++ b/packages/accounts/accounts_client.js @@ -1,4 +1,4 @@ -(function() { +(function () { Meteor.user = function () { if (Meteor.default_connection.userId()) { // XXX full identity? @@ -14,74 +14,12 @@ }); } - 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(); - return newwindow; - }; - - if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl) - throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setup first"); - - var oauthState = Meteor.uuid(); - - var popup = openCenteredPopup( - 'https://www.facebook.com/dialog/oauth?client_id=' + Meteor.accounts.facebook._appId + - '&redirect_uri=' + Meteor.accounts.facebook._appUrl + '/_oauth/facebook?close' + - '&scope=email&state=' + oauthState, - 1000, 600); // XXX should we use different dimensions, e.g. on mobile? - - var checkPopupOpen = setInterval(function() { - if (popup.closed) { - clearInterval(checkPopupOpen); - tryLoginAfterPopupClosed(oauthState); - } - }, 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(oauthState) { - Meteor.apply('login', [ - {oauth: {version: 2, provider: 'facebook', state: oauthState}} - ], {wait: true}, function(error, result) { - if (error) { - Meteor._debug("Server error on login", error); - return; - } - - Meteor.accounts.loginAndStoreToken(result.token); - callback && callback(); - }); - }; - Meteor.logout = function () { Meteor.apply('logout', [], {wait: true}, function(error, result) { - if (error) { - Meteor._debug("Server error on logout", error); - return; - } else { + if (error) + throw error; + else Meteor.accounts.forceClientLoggedOut(); - } }); }; })(); diff --git a/packages/accounts/accounts_common.js b/packages/accounts/accounts_common.js index 9a15b34269..297949567b 100644 --- a/packages/accounts/accounts_common.js +++ b/packages/accounts/accounts_common.js @@ -4,21 +4,8 @@ if (!Meteor.accounts) { Meteor.accounts = {}; } -if (!Meteor.accounts.facebook) { - Meteor.accounts.facebook = {}; -} - Meteor.accounts._loginTokens = new Meteor.Collection( "accounts._loginTokens", null /*manager*/, null /*driver*/, true /*preventAutopublish*/); - -Meteor.accounts.facebook.setup = function(appId, appUrl) { - Meteor.accounts.facebook._appId = appId; - Meteor.accounts.facebook._appUrl = appUrl; -}; - -Meteor.accounts.facebook.SetupError = function(description) { - this.message = description; -}; diff --git a/packages/accounts/accounts_server.js b/packages/accounts/accounts_server.js index 81571f4b7e..a4fc96acfb 100644 --- a/packages/accounts/accounts_server.js +++ b/packages/accounts/accounts_server.js @@ -1,53 +1,65 @@ -(function() { +(function () { + // Updates or creates a user after we authenticate with a 3rd party + // @param serviceName {String} e.g. 'facebook' or 'google' + // @param serviceUserId {?} user id in 3rd party service + // @param more {Object} additional attributes to store on the user record + // @returns {String} userId + Meteor.accounts.updateOrCreateUser = function(email, + serviceName, + serviceUserId, + more) { - var connect = __meteor_bootstrap__.require("connect"); + var userByEmail = Meteor.users.findOne({emails: email}); + if (userByEmail) { - // Incoming OAuth http requests are recorded here when the OAuth - // process is completed inside a popup window. Afterwards, these are - // read by the OAuth login method to complete the process. - // - // @type {Object} maps from Oauth "state" to request - Meteor.accounts._unmatchedOauthRequests = {}; + // If we know about this email address that is our user. + // Update the information from this service. + var user = userByEmail; + if (!user.services || !user.services[serviceName]) { + var attrs = {}; + attrs["services." + serviceName] = _.extend( + {id: serviceUserId}, more); + Meteor.users.update(user, {$set: attrs}); + } + return user._id; + } else { - Meteor.accounts.facebook.setSecret = function(secret) { - Meteor.accounts.facebook._secret = secret; + // If not, look for a user with the appropriate service user id. + // Update the user's email. + var selector = {}; + selector["services." + serviceName + ".id"] = serviceUserId; + var userByServiceUserId = Meteor.users.findOne(selector); + if (userByServiceUserId) { + var user = userByServiceUserId; + if (user.emails.indexOf(email) === -1) { + // The user may have changed the email address associated with + // this service. Store the new one in addition to the old one. + Meteor.users.update(user, {$push: {emails: email}}); + } + return user._id; + } else { + + // Create a new user + var attrs = {}; + attrs[serviceName] = _.extend({id: serviceUserId}, more); + return Meteor.users.insert({ + emails: [email], + services: attrs + }); + } + } }; - // 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(); - return; - } + Meteor.accounts._loginHandlers = []; - if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl) - throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setup first"); - if (!Meteor.accounts.facebook._secret) - throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setSecret first"); - - Meteor.accounts._unmatchedOauthRequests[req.query.state] = req; - - // We support /_oauth?close, /_oauth?redirect=URL. Any other /_oauth request - // just served 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'}); - var content = - ''; - res.end(content, 'utf-8'); - } else if (req.query.redirect) { - res.writeHead(302, {'Location': req.query.redirect}); - res.end(); - } else { - res.writeHead(200, {'Content-Type': 'text/html'}); - res.end(content, 'utf-8'); - } - }).run(); - }); + // @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} @@ -55,73 +67,9 @@ // If unsuccessful (for example, if the user closed the oauth login popup), // returns null 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 Meteor.Error("We only support facebook login for now. More soon!"); - - var fbAccessToken; - var unmatchedRequest = Meteor.accounts._unmatchedOauthRequests[options.oauth.state]; - if (unmatchedRequest) { - // We had previously received the HTTP request with the OAuth code - fbAccessToken = handleOauthRequest(unmatchedRequest); - delete Meteor.accounts._unmatchedOauthRequests[options.oauth.state]; - - // If the user didn't authorize the login, either explicitly - // or by closing the popup window, return null - if (!fbAccessToken) - return null; - } else { - return null; - } - - // 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 (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); @@ -131,7 +79,10 @@ id: this.userId() }; } else { - throw new Meteor.Error("Unrecognized options for login request"); + var result = tryAllLoginHandlers(options); + if (result !== null) + this.setUserId(result.id); + return result; } }, @@ -140,57 +91,28 @@ } }); - // @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') { - if (req.query.error) { - // Either the user didn't authorize access or we cancelled - // this outstanding login request (such as when the user - // closes the login popup window) - return null; - } + // 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; - // Request an access token - var response = Meteor.http.get( - "https://graph.facebook.com/oauth/access_token?" + - "client_id=" + Meteor.accounts.facebook._appId + - "&redirect_uri=" + Meteor.accounts.facebook._appUrl + "/_oauth/facebook?close" + - "&client_secret=" + Meteor.accounts.facebook._secret + - "&code=" + req.query.code).content; + _.find(Meteor.accounts._loginHandlers, function(handler) { - // Errors come back as JSON but success looks like a query encoded in a url - var error_response = null; - try { - // Just try to parse so that we know if we failed or not, - // while storing the parsed results - var error_response = JSON.parse(response); - } catch (e) { - } - - if (error_response) { - if (error_response.error) { - throw new Meteor.Error("Error trying to get access token from Facebook", error_response); - } else { - throw new Meteor.Error("Unexpected response when trying to get access token from Facebook", error_response); - } + var maybeResult = handler(options); + if (maybeResult !== undefined) { + result = maybeResult; + return true; } else { - // Success! 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; + return false; } + }); + + if (result === undefined) { + throw new Meteor.Error("Unrecognized options for login request"); } else { - throw new Meteor.Error("Unknown OAuth provider: " + provider); + return result; } }; -})(); +}) (); diff --git a/packages/accounts/localstorage_token.js b/packages/accounts/localstorage_token.js index 781ab3dbc1..b59071020e 100644 --- a/packages/accounts/localstorage_token.js +++ b/packages/accounts/localstorage_token.js @@ -28,18 +28,17 @@ Meteor.loginFromLocalStorage = function () { Meteor.accounts._lastLoginTokenWhenPolled = loginToken; if (loginToken) { Meteor.apply('login', [{resume: loginToken}], {wait: true}, function(error, result) { - if (error) { - Meteor._debug("Server error on login", error); - return; - } + if (error) + throw error; Meteor.default_connection.setUserId(result.id); Meteor.default_connection.onReconnect = function() { Meteor.apply('login', [{resume: loginToken}], {wait: true}, function(error, result) { if (error) { Meteor.accounts.forceClientLoggedOut(); - Meteor._debug("Server error on login", error); - return; + throw error; + } else { + // nothing to do } }); }; diff --git a/packages/accounts/package.js b/packages/accounts/package.js index 1da6e15120..8b189d48f4 100644 --- a/packages/accounts/package.js +++ b/packages/accounts/package.js @@ -3,7 +3,7 @@ Package.describe({ }); Package.on_use(function(api) { - api.use('http', ['client', 'server']); + api.use('underscore', 'server'); api.use('localstorage-polyfill', 'client'); api.add_files('accounts_common.js', ['client', 'server']); diff --git a/packages/localstorage-polyfill/localstorage_polyfill.js b/packages/localstorage-polyfill/localstorage_polyfill.js index d8d473136e..c3b83d403b 100644 --- a/packages/localstorage-polyfill/localstorage_polyfill.js +++ b/packages/localstorage-polyfill/localstorage_polyfill.js @@ -1,6 +1,7 @@ Meteor.startup(function() { // Since we need document.body to be defined if (!window.localStorage) { window.localStorage = (function () { + // XXX eliminate dependency on jQuery, detect browsers ourselves if ($.browser.msie) { // If we are on IE, which support userData var userdata = document.createElement('span'); // could be anything userdata.style.behavior = 'url("#default#userData")'; @@ -30,8 +31,8 @@ Meteor.startup(function() { // Since we need document.body to be defined } else { Meteor._debug( "You are running a browser with no localStorage or userData " - + "support (presumable Opera Mini). Logging in from one tab " - + "will not cause another tab to be logged in."); + + "support. Logging in from one tab will not cause another " + + "tab to be logged in."); return { setItem: function () {}, diff --git a/packages/oauth2/oauth2_client.js b/packages/oauth2/oauth2_client.js new file mode 100644 index 0000000000..a1d13f9580 --- /dev/null +++ b/packages/oauth2/oauth2_client.js @@ -0,0 +1,56 @@ +(function () { + Meteor.accounts.oauth2.initiateLogin = function(state, url) { + // XXX should we use different dimensions, e.g. on mobile? + var popup = openCenteredPopup(url, 1000, 600); + + var checkPopupOpen = setInterval(function() { + if (popup.closed) { + clearInterval(checkPopupOpen); + tryLoginAfterPopupClosed(state); + } + }, 100); + }; + + 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(); + 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_common.js b/packages/oauth2/oauth2_common.js new file mode 100644 index 0000000000..cb23a48c2d --- /dev/null +++ b/packages/oauth2/oauth2_common.js @@ -0,0 +1 @@ +Meteor.accounts.oauth2 = {}; \ No newline at end of file diff --git a/packages/oauth2/oauth2_server.js b/packages/oauth2/oauth2_server.js new file mode 100644 index 0000000000..02ad2308e4 --- /dev/null +++ b/packages/oauth2/oauth2_server.js @@ -0,0 +1,71 @@ +(function () { + var connect = __meteor_bootstrap__.require("connect"); + + Meteor.accounts.oauth2.providers = {}; + + Meteor.accounts.registerLoginHandler(function (options) { + if (!options.oauth) + return undefined; // don't handle + + var result = Meteor.accounts.oauth2.loginResultForState[options.oauth.state]; + if (result === undefined) // not using `!result` since can be null + // We weren't notified of the user authorizing the login. + return null; + else + 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` + Meteor.accounts.oauth2.loginResultForState = {}; + + // Listen on /_oauth/* + __meteor_bootstrap__.app + .use(connect.query()) + .use(function (req, res, next) { + Fiber(function() { + var bareUrl = req.url.substring(0, req.url.indexOf('?')); + var splitUrl = bareUrl.split('/'); + + // Any non-oauth request will continue down the default middlewares + if (splitUrl[1] !== '_oauth') { + next(); + return; + } + + // Make sure we prepare the login results before returning. + // This way the subsequent call to the `login` method will be + // immediate. + + var providerName = splitUrl[2]; + var provider = Meteor.accounts.oauth2.providers[providerName]; + // Get or create user id + var userId = provider.userIdForOauthReq(req); + // Generate and store a login token for reconnect + var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); + // Store results to subsequent call to `login` + 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 + 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'}); + var content = + ''; + res.end(content, 'utf-8'); + } else if (req.query.redirect) { + res.writeHead(302, {'Location': req.query.redirect}); + res.end(); + } else { + res.writeHead(200, {'Content-Type': 'text/html'}); + res.end(content, 'utf-8'); + } + }).run(); + }); + +})(); \ No newline at end of file diff --git a/packages/oauth2/package.js b/packages/oauth2/package.js new file mode 100644 index 0000000000..1a184ded51 --- /dev/null +++ b/packages/oauth2/package.js @@ -0,0 +1,12 @@ +Package.describe({ + summary: "A basis for OAuth2-based account systems", +}); + +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']); + api.add_files('oauth2_server.js', 'server'); + api.add_files('oauth2_client.js', 'client'); +}); From 2657787fd7ed6b20c5515225aa47531e0f17022b Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 13 Jun 2012 14:22:23 -0700 Subject: [PATCH 015/239] Move login button code from todos into new login buttons package. --- examples/todos/.meteor/packages | 1 + examples/todos/client/todos.css | 18 ------------- examples/todos/client/todos.html | 12 +-------- examples/todos/client/todos.js | 30 ---------------------- packages/login-buttons/login-buttons.css | 18 +++++++++++++ packages/login-buttons/login-buttons.html | 10 ++++++++ packages/login-buttons/login-buttons.js | 31 +++++++++++++++++++++++ packages/login-buttons/package.js | 12 +++++++++ 8 files changed, 73 insertions(+), 59 deletions(-) create mode 100644 packages/login-buttons/login-buttons.css create mode 100644 packages/login-buttons/login-buttons.html create mode 100644 packages/login-buttons/login-buttons.js create mode 100644 packages/login-buttons/package.js diff --git a/examples/todos/.meteor/packages b/examples/todos/.meteor/packages index d35f3979f0..f97290db15 100644 --- a/examples/todos/.meteor/packages +++ b/examples/todos/.meteor/packages @@ -8,3 +8,4 @@ backbone accounts accounts-facebook accounts-google +login-buttons diff --git a/examples/todos/client/todos.css b/examples/todos/client/todos.css index 7fff5e43d1..bad558c656 100644 --- a/examples/todos/client/todos.css +++ b/examples/todos/client/todos.css @@ -259,24 +259,6 @@ h3 { width: 80px; } -/* XXX remove once we have the login-buttons package */ -.fb-login, #logout { - cursor: pointer; - margin: 5px 10px 5px 5px; - padding: 2px 7px; - font-size: 80%; - color: white; - - background: #3B5998; - margin-top: 10px; - - border: 1px solid #777; - border-radius: 4px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -o-border-radius: 4px; -} - .toggle-privacy-wrapper { float: right; width: 110px; diff --git a/examples/todos/client/todos.html b/examples/todos/client/todos.html index d5456fcd88..8e0b987f3a 100644 --- a/examples/todos/client/todos.html +++ b/examples/todos/client/todos.html @@ -112,17 +112,7 @@ {{/each}}
- {{> login}} + {{> login_buttons}}
- - - diff --git a/examples/todos/client/todos.js b/examples/todos/client/todos.js index 6f45cb1f71..d6c39bccf9 100644 --- a/examples/todos/client/todos.js +++ b/examples/todos/client/todos.js @@ -299,36 +299,6 @@ Template.tag_filter.events = { } }; -////////// Login ////////// - -Template.login.events = { - 'click #fb-login': function () { - try { - Meteor.loginWithFacebook(); - } catch (e) { - if (e instanceof Meteor.accounts.facebook.SetupError) - alert("You haven't set up your Facebook app details. See fb-app.js and server/fb-secret.js"); - else - throw e; - } - }, - - 'click #google-login': function () { - try { - Meteor.loginWithGoogle(); - } catch (e) { - if (e instanceof Meteor.accounts.google.SetupError) - alert("You haven't set up your Google API details. See google-api.js and server/google-secret.js"); - else - throw e; - }; - }, - - 'click #logout': function() { - Meteor.logout(); - } -}; - ////////// Tracking selected list in URL ////////// var TodosRouter = Backbone.Router.extend({ diff --git a/packages/login-buttons/login-buttons.css b/packages/login-buttons/login-buttons.css new file mode 100644 index 0000000000..cae539969b --- /dev/null +++ b/packages/login-buttons/login-buttons.css @@ -0,0 +1,18 @@ +.login-button { + float:left; + + cursor: pointer; + margin: 5px 10px 5px 5px; + padding: 2px 7px; + font-size: 80%; + color: white; + + background: #3B5998; + margin-top: 10px; + + border: 1px solid #777; + border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; +} diff --git a/packages/login-buttons/login-buttons.html b/packages/login-buttons/login-buttons.html new file mode 100644 index 0000000000..d7f9a6b99e --- /dev/null +++ b/packages/login-buttons/login-buttons.html @@ -0,0 +1,10 @@ + diff --git a/packages/login-buttons/login-buttons.js b/packages/login-buttons/login-buttons.js new file mode 100644 index 0000000000..aed6b763b4 --- /dev/null +++ b/packages/login-buttons/login-buttons.js @@ -0,0 +1,31 @@ +(function () { + + Template.login_buttons.events = { + 'click #login-buttons-fb-login': function () { + try { + Meteor.loginWithFacebook(); + } catch (e) { + if (e instanceof Meteor.accounts.facebook.SetupError) + alert("You haven't set up your Facebook app details. See fb-app.js and server/fb-secret.js"); + else + throw e; + } + }, + + 'click #login-buttons-google-login': function () { + try { + Meteor.loginWithGoogle(); + } catch (e) { + if (e instanceof Meteor.accounts.google.SetupError) + alert("You haven't set up your Google API details. See google-api.js and server/google-secret.js"); + else + throw e; + }; + }, + + 'click #login-buttons-logout': function() { + Meteor.logout(); + } + }; + +})(); diff --git a/packages/login-buttons/package.js b/packages/login-buttons/package.js new file mode 100644 index 0000000000..6b331da121 --- /dev/null +++ b/packages/login-buttons/package.js @@ -0,0 +1,12 @@ +Package.describe({ + summary: "Simple template to add login buttons to an app." +}); + +Package.on_use(function (api) { + api.use(['accounts', 'underscore', 'liveui', 'templating'], 'client'); + + api.add_files([ + 'login-buttons.css', + 'login-buttons.html', + 'login-buttons.js'], 'client'); +}); From 9e369b960475ab2d8d4374d2731ebf052563f94e Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 13 Jun 2012 17:06:36 -0700 Subject: [PATCH 016/239] Store username and display in login-buttons --- packages/accounts-facebook/facebook_server.js | 4 +- packages/accounts-google/google_server.js | 4 +- packages/accounts/accounts_client.js | 18 ++++++-- packages/accounts/accounts_server.js | 41 +++++++++++++++---- packages/login-buttons/login-buttons.css | 6 +++ packages/login-buttons/login-buttons.html | 1 + packages/login-buttons/login-buttons.js | 14 +++++++ 7 files changed, 74 insertions(+), 14 deletions(-) diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index 51efee1cb8..56d6113706 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -23,8 +23,8 @@ params: {access_token: accessToken}}).data; return Meteor.accounts.updateOrCreateUser( - identity.email, 'facebook', identity.id, - {accessToken: accessToken}); + identity.email, {name: identity.name}, + 'facebook', identity.id, {accessToken: accessToken}); } }; diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index af40eb392c..9b91268af1 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -16,8 +16,8 @@ {params: {access_token: accessToken}}).data; return Meteor.accounts.updateOrCreateUser( - identity.email, 'google', identity.id, - {accessToken: accessToken}); + identity.email, {name: identity.name}, + 'google', identity.id, {accessToken: accessToken}); } }; diff --git a/packages/accounts/accounts_client.js b/packages/accounts/accounts_client.js index 1f93cfa5ac..d5381eba2a 100644 --- a/packages/accounts/accounts_client.js +++ b/packages/accounts/accounts_client.js @@ -1,8 +1,18 @@ (function () { + Meteor.user = function () { - if (Meteor.default_connection.userId()) { - // XXX full identity? - return {_id: Meteor.default_connection.userId()}; + + var userId = Meteor.default_connection.userId(); + if (userId) { + var result = Meteor.users.findOne(userId); + if (result) { + return result; + } else { + // If the login method completes but new subcriptions haven't + // yet been sent down to the client, this is the best we can + // do + return {_id: userId}; + } } else { return null; } @@ -22,4 +32,6 @@ Meteor.accounts.forceClientLoggedOut(); }); }; + + Meteor.subscribe("currentUser"); })(); diff --git a/packages/accounts/accounts_server.js b/packages/accounts/accounts_server.js index a4fc96acfb..97581b39e0 100644 --- a/packages/accounts/accounts_server.js +++ b/packages/accounts/accounts_server.js @@ -1,26 +1,43 @@ (function () { // Updates or creates a user after we authenticate with a 3rd party + // + // @param email {String} The user's email + // @param userData {Object} attributes to store directly on the user object // @param serviceName {String} e.g. 'facebook' or 'google' // @param serviceUserId {?} user id in 3rd party service - // @param more {Object} additional attributes to store on the user record + // @param serviceData {Object} attributes to store on the user record's + // specific service subobject // @returns {String} userId Meteor.accounts.updateOrCreateUser = function(email, + userData, serviceName, serviceUserId, - more) { + serviceData) { + var updateUserData = function() { + // don't overwrite existing fields + var newKeys = _.without(_.keys(userData), _.keys(user)); + var newAttrs = _.pick(userData, newKeys); + Meteor.users.update(user, {$set: newAttrs}); + }; - var userByEmail = Meteor.users.findOne({emails: email}); + if (!email) + throw new Meteor.Error("We don't yet support email-less users"); + + var userByEmail = Meteor.users.findOne({emails: userData.email}); + var user; if (userByEmail) { // If we know about this email address that is our user. // Update the information from this service. - var user = userByEmail; + user = userByEmail; if (!user.services || !user.services[serviceName]) { var attrs = {}; attrs["services." + serviceName] = _.extend( {id: serviceUserId}, more); Meteor.users.update(user, {$set: attrs}); } + + updateUserData(); return user._id; } else { @@ -30,22 +47,24 @@ selector["services." + serviceName + ".id"] = serviceUserId; var userByServiceUserId = Meteor.users.findOne(selector); if (userByServiceUserId) { - var user = userByServiceUserId; + user = userByServiceUserId; if (user.emails.indexOf(email) === -1) { // The user may have changed the email address associated with // this service. Store the new one in addition to the old one. Meteor.users.update(user, {$push: {emails: email}}); } + + updateUserData(); return user._id; } else { // Create a new user var attrs = {}; attrs[serviceName] = _.extend({id: serviceUserId}, more); - return Meteor.users.insert({ + return Meteor.users.insert(_.extend({}, userData, { emails: [email], services: attrs - }); + })); } } }; @@ -91,6 +110,14 @@ } }); + // Publish a few attributes on the current user object + Meteor.publish("currentUser", function() { + if (this.userId()) + return Meteor.users.find({_id: this.userId()}, {emails: 1, name: 1}); + else + return null; + }); + // 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. diff --git a/packages/login-buttons/login-buttons.css b/packages/login-buttons/login-buttons.css index cae539969b..a094373e78 100644 --- a/packages/login-buttons/login-buttons.css +++ b/packages/login-buttons/login-buttons.css @@ -1,3 +1,9 @@ +.login-header-email { + float: left; + padding: 2px 7px; + margin: 5px 0px 5px 5px; +} + .login-button { float:left; diff --git a/packages/login-buttons/login-buttons.html b/packages/login-buttons/login-buttons.html index d7f9a6b99e..a779187fdf 100644 --- a/packages/login-buttons/login-buttons.html +++ b/packages/login-buttons/login-buttons.html @@ -1,6 +1,7 @@ diff --git a/packages/login-buttons/login-buttons-images.css b/packages/accounts-ui/login-buttons-images.css similarity index 100% rename from packages/login-buttons/login-buttons-images.css rename to packages/accounts-ui/login-buttons-images.css diff --git a/packages/login-buttons/login-buttons.css b/packages/accounts-ui/login-buttons.css similarity index 100% rename from packages/login-buttons/login-buttons.css rename to packages/accounts-ui/login-buttons.css diff --git a/packages/login-buttons/login-buttons.html b/packages/accounts-ui/login-buttons.html similarity index 94% rename from packages/login-buttons/login-buttons.html rename to packages/accounts-ui/login-buttons.html index d4f43eb333..3db160f3f3 100644 --- a/packages/login-buttons/login-buttons.html +++ b/packages/accounts-ui/login-buttons.html @@ -1,4 +1,4 @@ - - - + + {{> resetPasswordForm}} {{> enrollAccountForm}} {{> justValidatedUserForm}} + {{> configureLoginServicesDialog}} diff --git a/packages/accounts-ui/login_buttons.js b/packages/accounts-ui/login_buttons.js index 64be104617..da00f2068c 100644 --- a/packages/accounts-ui/login_buttons.js +++ b/packages/accounts-ui/login_buttons.js @@ -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 // diff --git a/packages/accounts-ui/login_buttons.less b/packages/accounts-ui/login_buttons.less index e16371f657..9403ca3cb6 100644 --- a/packages/accounts-ui/login_buttons.less +++ b/packages/accounts-ui/login_buttons.less @@ -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; +} + + diff --git a/packages/accounts-weibo/package.js b/packages/accounts-weibo/package.js index d1cad4e758..c178954131 100644 --- a/packages/accounts-weibo/package.js +++ b/packages/accounts-weibo/package.js @@ -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'); diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js index 9371dc08cd..c9dcf36b0e 100644 --- a/packages/accounts-weibo/weibo_client.js +++ b/packages/accounts-weibo/weibo_client.js @@ -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); diff --git a/packages/accounts-weibo/weibo_configure.html b/packages/accounts-weibo/weibo_configure.html new file mode 100644 index 0000000000..a509880207 --- /dev/null +++ b/packages/accounts-weibo/weibo_configure.html @@ -0,0 +1,25 @@ + diff --git a/packages/accounts-weibo/weibo_configure.js b/packages/accounts-weibo/weibo_configure.js new file mode 100644 index 0000000000..1f13efdce7 --- /dev/null +++ b/packages/accounts-weibo/weibo_configure.js @@ -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'} + ]; +}; \ No newline at end of file diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index 822ea48c44..1c95214001 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -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' }}); From 69110dec0f90877a9294057a4572876d0dae37e0 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Thu, 20 Sep 2012 22:11:11 -0700 Subject: [PATCH 141/239] Add callback to oauth login methods. Display errors in accounts-ui. --- packages/accounts-base/accounts_common.js | 12 +++- packages/accounts-facebook/facebook_client.js | 10 ++-- packages/accounts-google/google_client.js | 10 ++-- .../accounts-oauth-helper/oauth_client.js | 33 ++++++---- packages/accounts-twitter/twitter_client.js | 10 ++-- packages/accounts-ui/login_buttons.js | 60 ++++++++++++------- packages/accounts-weibo/weibo_client.js | 10 ++-- 7 files changed, 98 insertions(+), 47 deletions(-) diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index 3aaf9a7062..68f61e67cc 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -44,4 +44,14 @@ Meteor.accounts.ConfigError = function(description) { this.message = description; }; Meteor.accounts.ConfigError.prototype = new Error(); -Meteor.accounts.ConfigError.prototype.name = 'Meteor.accounts.ConfigError'; \ No newline at end of file +Meteor.accounts.ConfigError.prototype.name = 'Meteor.accounts.ConfigError'; + +// Thrown when the user cancels the login process (eg, closes an oauth +// popup, declines retina scan, etc) +Meteor.accounts.LoginCancelledError = function(description) { + this.message = description; + this.cancelled = true; +}; +Meteor.accounts.LoginCancelledError.prototype = new Error(); +Meteor.accounts.LoginCancelledError.prototype.name = 'Meteor.accounts.LoginCancelledError'; + diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js index d3901c310a..e92e3cbd1a 100644 --- a/packages/accounts-facebook/facebook_client.js +++ b/packages/accounts-facebook/facebook_client.js @@ -1,8 +1,10 @@ (function () { - Meteor.loginWithFacebook = function () { + Meteor.loginWithFacebook = function (callback) { var config = Meteor.accounts.configuration.findOne({service: 'facebook'}); - if (!config) - throw new Meteor.accounts.ConfigError("Service not configured"); + if (!config) { + callback && callback(new Meteor.accounts.ConfigError("Service not configured")); + return; + } var state = Meteor.uuid(); var mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent); @@ -18,7 +20,7 @@ '&redirect_uri=' + Meteor.absoluteUrl('_oauth/facebook?close') + '&display=' + display + '&scope=' + scope + '&state=' + state; - Meteor.accounts.oauth.initiateLogin(state, loginUrl); + Meteor.accounts.oauth.initiateLogin(state, loginUrl, callback); }; })(); diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index 97bd9cb7bb..81cc861de9 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -1,8 +1,10 @@ (function () { - Meteor.loginWithGoogle = function () { + Meteor.loginWithGoogle = function (callback) { var config = Meteor.accounts.configuration.findOne({service: 'google'}); - if (!config) - throw new Meteor.accounts.ConfigError("Service not configured"); + if (!config) { + callback && callback(new Meteor.accounts.ConfigError("Service not configured")); + return; + } var state = Meteor.uuid(); @@ -26,7 +28,7 @@ '&redirect_uri=' + Meteor.absoluteUrl('_oauth/google?close') + '&state=' + state; - Meteor.accounts.oauth.initiateLogin(state, loginUrl); + Meteor.accounts.oauth.initiateLogin(state, loginUrl, callback); }; }) (); diff --git a/packages/accounts-oauth-helper/oauth_client.js b/packages/accounts-oauth-helper/oauth_client.js index f28bd44a97..763390df01 100644 --- a/packages/accounts-oauth-helper/oauth_client.js +++ b/packages/accounts-oauth-helper/oauth_client.js @@ -3,7 +3,10 @@ // // @param state {String} The OAuth state generated by the client // @param url {String} url to page - Meteor.accounts.oauth.initiateLogin = function(state, url) { + // @param callback {Function} Callback function to call on + // completion. Takes one argument, null on success, or Error on + // error. + Meteor.accounts.oauth.initiateLogin = function(state, url, callback) { // XXX these dimensions worked well for facebook and google, but // it's sort of weird to have these here. Maybe an optional // argument instead? @@ -15,7 +18,7 @@ // http://code.google.com/p/android/issues/detail?id=21061 if (popup.closed || popup.closed === undefined) { clearInterval(checkPopupOpen); - tryLoginAfterPopupClosed(state); + tryLoginAfterPopupClosed(state, callback); } }, 100); }; @@ -23,19 +26,29 @@ // 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) { + var tryLoginAfterPopupClosed = function(state, callback) { Meteor.apply('login', [ {oauth: {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; + if (error) { + // got an error from the server. report it back. + callback && callback(error); + } else if (!result) { + // got an empty response from the server. This means our oauth + // state wasn't recognized, which could be either because the + // popup was closed by the user before completion, or some sort + // of error where the oauth provider didn't talk to our server + // correctly and closed the popup somehow. + // + // we assume it was user canceled, and report it as such. this + // will mask failures where things are misconfigured such that + // the server doesn't see the request but does close the + // window. This seems unlikely. + callback && + callback(new Meteor.accounts.LoginCancelledError("Popup closed")); } else { Meteor.accounts.makeClientLoggedIn(result.id, result.token); + callback && callback(); } }); }; diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js index 4e64ed5050..e21a107452 100644 --- a/packages/accounts-twitter/twitter_client.js +++ b/packages/accounts-twitter/twitter_client.js @@ -1,8 +1,10 @@ (function () { - Meteor.loginWithTwitter = function () { + Meteor.loginWithTwitter = function (callback) { var config = Meteor.accounts.configuration.findOne({service: 'twitter'}); - if (!config) - throw new Meteor.accounts.ConfigError("Service not configured"); + if (!config) { + callback && callback(new Meteor.accounts.ConfigError("Service not configured")); + return; + } var state = Meteor.uuid(); // We need to keep state across the next two 'steps' so we're adding @@ -19,7 +21,7 @@ + encodeURIComponent(callbackUrl) + '&state=' + state; - Meteor.accounts.oauth.initiateLogin(state, url); + Meteor.accounts.oauth.initiateLogin(state, url, callback); }; })(); diff --git a/packages/accounts-ui/login_buttons.js b/packages/accounts-ui/login_buttons.js index da00f2068c..6861b73231 100644 --- a/packages/accounts-ui/login_buttons.js +++ b/packages/accounts-ui/login_buttons.js @@ -41,35 +41,55 @@ Template.loginButtons.events = { 'click #login-buttons-Facebook': function () { - if (Meteor.accounts.configuration.findOne({service: 'facebook'})) { - Meteor.loginWithFacebook(); - } else { - configureService("Facebook"); // XXX refactor "Facebook" -> "facebook" - } + resetMessages(); + Meteor.loginWithFacebook(function (e) { + if (!e || e instanceof Meteor.accounts.LoginCancelledError) { + // do nothing + } else if (e instanceof Meteor.accounts.ConfigError) { + configureService("Facebook"); // XXX refactor "Facebook" -> "facebook" + } else { + Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error"); + } + }); }, 'click #login-buttons-Google': function () { - if (Meteor.accounts.configuration.findOne({service: 'google'})) { - Meteor.loginWithGoogle(); - } else { - configureService("Google"); - } + resetMessages(); + Meteor.loginWithGoogle(function (e) { + if (!e || e instanceof Meteor.accounts.LoginCancelledError) { + // do nothing + } else if (e instanceof Meteor.accounts.ConfigError) { + configureService("Google"); + } else { + Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error"); + } + }); }, 'click #login-buttons-Weibo': function () { - if (Meteor.accounts.configuration.findOne({service: 'weibo'})) { - Meteor.loginWithWeibo(); - } else { - configureService("Weibo"); - } + resetMessages(); + Meteor.loginWithWeibo(function (e) { + if (!e || e instanceof Meteor.accounts.LoginCancelledError) { + // do nothing + } else if (e instanceof Meteor.accounts.ConfigError) { + configureService("Weibo"); + } else { + Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error"); + } + }); }, 'click #login-buttons-Twitter': function () { - if (Meteor.accounts.configuration.findOne({service: 'twitter'})) { - Meteor.loginWithTwitter(); - } else { - configureService("Twitter"); - } + resetMessages(); + Meteor.loginWithTwitter(function (e) { + if (!e || e instanceof Meteor.accounts.LoginCancelledError) { + // do nothing + } else if (e instanceof Meteor.accounts.ConfigError) { + configureService("Twitter"); + } else { + Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error"); + } + }); }, 'click #login-buttons-logout': function() { diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js index c9dcf36b0e..9cc2bfaf94 100644 --- a/packages/accounts-weibo/weibo_client.js +++ b/packages/accounts-weibo/weibo_client.js @@ -1,8 +1,10 @@ (function () { - Meteor.loginWithWeibo = function () { + Meteor.loginWithWeibo = function (callback) { var config = Meteor.accounts.configuration.findOne({service: 'weibo'}); - if (!config) - throw new Meteor.accounts.ConfigError("Service not configured"); + if (!config) { + callback && callback(new Meteor.accounts.ConfigError("Service not configured")); + return; + } var state = Meteor.uuid(); // XXX need to support configuring access_type and scope @@ -13,7 +15,7 @@ '&redirect_uri=' + Meteor.absoluteUrl('_oauth/weibo?close', {replaceLocalhost: true}) + '&state=' + state; - Meteor.accounts.oauth.initiateLogin(state, loginUrl); + Meteor.accounts.oauth.initiateLogin(state, loginUrl, callback); }; }) (); From 1a5cd631f6845798487ea903c4391a6fcc954524 Mon Sep 17 00:00:00 2001 From: Braden Simpson Date: Mon, 24 Sep 2012 16:03:00 -0700 Subject: [PATCH 142/239] Update to include Github auth in the core dialog. --- packages/accounts-ui/login_buttons.js | 17 ++++++++++++++++- packages/accounts-ui/login_buttons_images.css | 4 ++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/accounts-ui/login_buttons.js b/packages/accounts-ui/login_buttons.js index 6861b73231..6909c84077 100644 --- a/packages/accounts-ui/login_buttons.js +++ b/packages/accounts-ui/login_buttons.js @@ -65,7 +65,20 @@ } }); }, - + + 'click #login-buttons-Github': function () { + resetMessages(); + Meteor.loginWithGithub(function (e) { + if (!e || e instanceof Meteor.accounts.LoginCancelledError) { + // do nothing + } else if (e instanceof Meteor.accounts.ConfigError) { + configureService("Github"); + } else { + Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error"); + } + }); + }, + 'click #login-buttons-Weibo': function () { resetMessages(); Meteor.loginWithWeibo(function (e) { @@ -658,6 +671,8 @@ ret.push({name: 'Facebook'}); if (Meteor.accounts.google) ret.push({name: 'Google'}); + if (Meteor.accounts.github) + ret.push({name: 'Github'}); if (Meteor.accounts.weibo) ret.push({name: 'Weibo'}); if (Meteor.accounts.twitter) diff --git a/packages/accounts-ui/login_buttons_images.css b/packages/accounts-ui/login_buttons_images.css index 88f9b54546..b84742a8dc 100644 --- a/packages/accounts-ui/login_buttons_images.css +++ b/packages/accounts-ui/login_buttons_images.css @@ -15,3 +15,7 @@ #login-buttons-image-Twitter { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAByklEQVQ4jaVTz0sbQRh92V10l006GaKJCtEtmqMYU0Qpwqb4B6zgXdT0WEr7B0ih4MGLP05CUWMvHkQwglhvGhsvKmJOBhTUQjWU2slilKarrAfdZROTQ8m7fPMx33szb75vXKZpohpwVbEBCNaCMUYopXppAWOMxDNsOPf3H1WIeDoSURYYYwQAKKW6y7KgLe2vam11KyMRZcEpEP6SOkwbUgc4ATAKUF8YW2fXhZejvaHPsc7gvH2DnCfQGEtdxrd/5NRJteUDpVTf+5kLp2WlA6JsCyZv9ChplPKdTfJZkYWhEF3bvnV3fb36NZSY3dP6Q/5V4hFvIAaKPckE8W5pLBIQdwHAthBdPtpJuhpeAwDu74DrP4/R1/Ts4cwBWg/gN+DowoSqTBPezAMAeAHw+suSw4Q7schFApF6af19a+2yLVIB7xR+0Zk75yCveu82FMnMViKHCXcSa3PPVBJAX5BszL2SP2kNwvdy5M1e+S2AogME4HFYPibPpxKZC03nRAp/M+Dx2UWDzTXfpttrx72ikCoVtrrAAwgdXBk9iazxxtpskfhs1O86aHXXpAEcA7ivJGDBDcDnyAsA2FMsi1KB/0bVv/EBBBSY9mZ7PAsAAAAASUVORK5CYII=); } + +#login-buttons-image-Github { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII=); +} \ No newline at end of file From 0b2548a9466091dd553f0f45adfd40f695d6ce69 Mon Sep 17 00:00:00 2001 From: Braden Simpson Date: Mon, 24 Sep 2012 16:07:02 -0700 Subject: [PATCH 143/239] Formatting changes --- packages/accounts-ui/login_buttons.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/accounts-ui/login_buttons.js b/packages/accounts-ui/login_buttons.js index 6909c84077..bd41fe0e95 100644 --- a/packages/accounts-ui/login_buttons.js +++ b/packages/accounts-ui/login_buttons.js @@ -71,11 +71,11 @@ Meteor.loginWithGithub(function (e) { if (!e || e instanceof Meteor.accounts.LoginCancelledError) { // do nothing - } else if (e instanceof Meteor.accounts.ConfigError) { - configureService("Github"); - } else { + } else if (e instanceof Meteor.accounts.ConfigError) { + configureService("Github"); + } else { Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error"); - } + } }); }, From 55e291e671108114b717f2e02d707d207b1e1d4c Mon Sep 17 00:00:00 2001 From: Braden Simpson Date: Mon, 24 Sep 2012 19:56:16 -0700 Subject: [PATCH 144/239] Added the accounts-github files into the main meteor repo --- packages/accounts-github/README.md | 21 ++++++++ packages/accounts-github/github_client.js | 26 +++++++++ packages/accounts-github/github_common.js | 7 +++ .../accounts-github/github_configure.html | 22 ++++++++ packages/accounts-github/github_configure.js | 10 ++++ packages/accounts-github/github_server.js | 53 +++++++++++++++++++ packages/accounts-github/package.js | 18 +++++++ packages/accounts-github/smart.json | 8 +++ 8 files changed, 165 insertions(+) create mode 100644 packages/accounts-github/README.md create mode 100644 packages/accounts-github/github_client.js create mode 100644 packages/accounts-github/github_common.js create mode 100644 packages/accounts-github/github_configure.html create mode 100644 packages/accounts-github/github_configure.js create mode 100644 packages/accounts-github/github_server.js create mode 100644 packages/accounts-github/package.js create mode 100644 packages/accounts-github/smart.json diff --git a/packages/accounts-github/README.md b/packages/accounts-github/README.md new file mode 100644 index 0000000000..5d8ab2792e --- /dev/null +++ b/packages/accounts-github/README.md @@ -0,0 +1,21 @@ +## accounts-github + +Github OAuth2 login service for use with Meteor Auth + +### Package Dependencies + +This login service depends on the bleeding edge changes within the Meteor Auth branch. See [https://github.com/meteor/meteor/wiki/Getting-started-with-Auth](https://github.com/meteor/meteor/wiki/Getting-started-with-Auth) for further details. + +* accounts ([Meteor Auth Branch](https://github.com/meteor/meteor/wiki/Getting-started-with-Auth)) +* accounts-oauth2-helper ([Meteor Auth Branch](https://github.com/meteor/meteor/wiki/Getting-started-with-Auth)) +* http + +### Usage + +1. `meteor add accounts-github` +2. Read the 'Integrating with Login Services' section of [Getting Started with Auth](https://github.com/meteor/meteor/wiki/Getting-started-with-Auth) and make sure you set up your config and secret correctly. +3. Call `Meteor.loginWithGithub();` + +### Credits + +Shamelessly based upon [@possibilities](https://github.com/possibilities) Google OAuth2 login service diff --git a/packages/accounts-github/github_client.js b/packages/accounts-github/github_client.js new file mode 100644 index 0000000000..b8e854df6a --- /dev/null +++ b/packages/accounts-github/github_client.js @@ -0,0 +1,26 @@ +(function () { + Meteor.loginWithGithub = function (callback) { + var config = Meteor.accounts.configuration.findOne({service: 'github'}); + if (!config) { + callback && callback(new Meteor.accounts.ConfigError("Service not configured")); + return; + } + var state = Meteor.uuid(); + + var required_scope = ['user']; + var scope = []; + if (Meteor.accounts.github._options && Meteor.accounts.github._options.scope) + scope = Meteor.accounts.github._options.scope; + scope = _.union(scope, required_scope); + var flat_scope = _.map(scope, encodeURIComponent).join('+'); + + var loginUrl = + 'https://github.com/login/oauth/authorize' + + '?client_id=' + config.clientId + + '&scope=' + flat_scope + + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/github?close') + + '&state=' + state; + + Meteor.accounts.oauth.initiateLogin(state, loginUrl, callback); + }; +}) (); diff --git a/packages/accounts-github/github_common.js b/packages/accounts-github/github_common.js new file mode 100644 index 0000000000..7384700534 --- /dev/null +++ b/packages/accounts-github/github_common.js @@ -0,0 +1,7 @@ +if (!Meteor.accounts.github) { + Meteor.accounts.github = {}; +} + +Meteor.accounts.github.config = function(options) { + Meteor.accounts.github._options = options; +}; \ No newline at end of file diff --git a/packages/accounts-github/github_configure.html b/packages/accounts-github/github_configure.html new file mode 100644 index 0000000000..9c97c160c1 --- /dev/null +++ b/packages/accounts-github/github_configure.html @@ -0,0 +1,22 @@ + \ No newline at end of file diff --git a/packages/accounts-github/github_configure.js b/packages/accounts-github/github_configure.js new file mode 100644 index 0000000000..1468abba07 --- /dev/null +++ b/packages/accounts-github/github_configure.js @@ -0,0 +1,10 @@ +Template.configureLoginServicesDialogForGithub.siteUrl = function () { + return Meteor.absoluteUrl(); +}; + +Template.configureLoginServicesDialogForGithub.fields = function () { + return [ + {property: 'clientId', label: 'Client ID'}, + {property: 'secret', label: 'Client secret'} + ]; +}; \ No newline at end of file diff --git a/packages/accounts-github/github_server.js b/packages/accounts-github/github_server.js new file mode 100644 index 0000000000..efe273eb03 --- /dev/null +++ b/packages/accounts-github/github_server.js @@ -0,0 +1,53 @@ +(function () { + Meteor.accounts.github.setSecret = function (secret) { + Meteor.accounts.github._secret = secret; + }; + + Meteor.accounts.oauth.registerService('github', 2, function(query) { + + var accessToken = getAccessToken(query); + var identity = getIdentity(accessToken); + + return { + options: { + services: { + github: { + id: identity.id, + accessToken: accessToken, + email: identity.email + }} + }, + extra: {profile: {name: identity.name}} + }; + }); + + var getAccessToken = function (query) { + var config = Meteor.accounts.configuration.findOne({service: 'github'}); + if (!config) + throw new Meteor.accounts.ConfigError("Service not configured"); + + var result = Meteor.http.post( + "https://github.com/login/oauth/access_token", {headers: {Accept: 'application/json'}, params: { + code: query.code, + client_id: config.clientId, + client_secret: config.secret, + redirect_uri: Meteor.absoluteUrl("_oauth/github?close"), + state: query.state + }}); + if (result.error) // if the http response was an error + throw result.error; + if (result.data.error) // if the http response was a json object with an error attribute + throw result.data; + return result.data.access_token; + }; + + var getIdentity = function (accessToken) { + var result = Meteor.http.get( + "https://api.github.com/user", + {params: {access_token: accessToken}}); + + if (result.error) + throw result.error; + return result.data; + }; +}) (); \ No newline at end of file diff --git a/packages/accounts-github/package.js b/packages/accounts-github/package.js new file mode 100644 index 0000000000..76cd6316f4 --- /dev/null +++ b/packages/accounts-github/package.js @@ -0,0 +1,18 @@ +Package.describe({ + summary: "Login service for Github accounts" +}); + +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( + ['github_configure.html', 'github_configure.js'], + 'client'); + + api.add_files('github_common.js', ['client', 'server']); + api.add_files('github_server.js', 'server'); + api.add_files('github_client.js', 'client'); +}); diff --git a/packages/accounts-github/smart.json b/packages/accounts-github/smart.json new file mode 100644 index 0000000000..a7d1ebec89 --- /dev/null +++ b/packages/accounts-github/smart.json @@ -0,0 +1,8 @@ +{ + "name": "accounts-github", + "description": "Github OAuth2 login service for use with Meteor Auth", + "homepage": "https://github.com/Jabbslad/accounts-github", + "author": "Jamie Atkinson ", + "version": "0.1.0", + "git": "https://github.com/Jabbslad/accounts-github.git" +} \ No newline at end of file From 6da48338852d5c7899b5b3433cf3c0fc280d8f9a Mon Sep 17 00:00:00 2001 From: Braden Simpson Date: Thu, 27 Sep 2012 11:19:10 -0700 Subject: [PATCH 145/239] added username to the identity. --- packages/accounts-github/github_server.js | 4 ++-- packages/accounts-ui/login_buttons.js | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/accounts-github/github_server.js b/packages/accounts-github/github_server.js index efe273eb03..2345941cd1 100644 --- a/packages/accounts-github/github_server.js +++ b/packages/accounts-github/github_server.js @@ -14,7 +14,8 @@ github: { id: identity.id, accessToken: accessToken, - email: identity.email + email: identity.email, + username: identity.login }} }, extra: {profile: {name: identity.name}} @@ -45,7 +46,6 @@ var result = Meteor.http.get( "https://api.github.com/user", {params: {access_token: accessToken}}); - if (result.error) throw result.error; return result.data; diff --git a/packages/accounts-ui/login_buttons.js b/packages/accounts-ui/login_buttons.js index bd41fe0e95..ba38876561 100644 --- a/packages/accounts-ui/login_buttons.js +++ b/packages/accounts-ui/login_buttons.js @@ -67,17 +67,17 @@ }, 'click #login-buttons-Github': function () { - resetMessages(); - Meteor.loginWithGithub(function (e) { - if (!e || e instanceof Meteor.accounts.LoginCancelledError) { - // do nothing - } else if (e instanceof Meteor.accounts.ConfigError) { - configureService("Github"); - } else { - Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error"); - } - }); - }, + resetMessages(); + Meteor.loginWithGithub(function (e) { + if (!e || e instanceof Meteor.accounts.LoginCancelledError) { + // do nothing + } else if (e instanceof Meteor.accounts.ConfigError) { + configureService("Github"); + } else { + Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error"); + } + }); + }, 'click #login-buttons-Weibo': function () { resetMessages(); From c0815f5061d9c552419cb5036d4a9b2748e8514e Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 27 Sep 2012 17:17:28 -0700 Subject: [PATCH 146/239] Fix #353: livedata_connection state corruption with "wait: true" methods. When a "wait: true" method is ready to fire its callback, and there are blocked methods but no blocked "wait" methods, we used to leave the blocked methods in the blocked_methods array in addition to sending them. In addition to leading to potentially leading to duplicate method calls (ignored by the server), this blocked all code push migrations. --- packages/livedata/livedata_connection.js | 2 ++ packages/livedata/livedata_connection_tests.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index de1b776719..b903a9a6da 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -648,6 +648,8 @@ _.extend(Meteor._LivedataConnection.prototype, { if (i !== self.blocked_methods.length) { self.outstanding_wait_method = self.blocked_methods[i]; self.blocked_methods = _.rest(self.blocked_methods, i+1); + } else { + self.blocked_methods = []; } // Send any new outstanding methods after we reshift the diff --git a/packages/livedata/livedata_connection_tests.js b/packages/livedata/livedata_connection_tests.js index 75cd6d56dd..0c4a40e8fb 100644 --- a/packages/livedata/livedata_connection_tests.js +++ b/packages/livedata/livedata_connection_tests.js @@ -484,6 +484,8 @@ Tinytest.add("livedata connection - one wait method with response out of order", test.equal(stream.sent.length, 1); var three_message = JSON.parse(stream.sent.shift()); test.equal(three_message.params, ['three!']); + // Since we sent it, it should no longer be in "blocked_methods". + test.equal(conn.blocked_methods, []); stream.receive({msg: 'result', id: three_message.id}); test.equal(stream.sent.length, 0); From 5def0ac65ff3460015e12399fbff642a5e9321b0 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 26 Sep 2012 15:35:25 -0700 Subject: [PATCH 147/239] Add 'Meteor.setPassword' on the server. Relax constraints around setting an initial password for users. --- .../accounts-password/passwords_server.js | 34 ++++++++++++++----- packages/accounts-password/passwords_tests.js | 30 ++++++++++++++++ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/packages/accounts-password/passwords_server.js b/packages/accounts-password/passwords_server.js index c8e73237c7..530e73763a 100644 --- a/packages/accounts-password/passwords_server.js +++ b/packages/accounts-password/passwords_server.js @@ -304,6 +304,15 @@ }); + Meteor.setPassword = function (userId, newPassword) { + var user = Meteor.users.findOne(userId); + if (!user) + throw new Meteor.Error(403, "User not found"); + var newVerifier = Meteor._srp.generateVerifier(newPassword); + + Meteor.users.update({_id: user._id}, { + $set: {'services.password.srp': newVerifier}}); + }; //////////// @@ -388,20 +397,27 @@ extra = {}; } - // XXX relax these constraints! - + // XXX allow an optional callback? if (callback) { throw new Error("Meteor.createUser with callback not supported on the server yet."); } - if (options.password || options.srp) - throw new Error("Meteor.createUser on the server does not let you set a password yet."); - - if (!options.email) - throw new Error("Meteor.createUser on the server requires email."); - var userId = createUser(options, extra); - Meteor.accounts.sendEnrollmentEmail(userId, options.email); + + // send email if the user has an email and no password + var user = Meteor.users.findOne(userId); + if ( + // user has email address + (user && user.emails && user.emails.length && + user.emails[0].address) && + // and does not have a password + !(user.services && user.services.password && + user.services.password.srp)) { + + var email = user.emails[0].address; + Meteor.accounts.sendEnrollmentEmail(userId, email); + } + return userId; }; diff --git a/packages/accounts-password/passwords_tests.js b/packages/accounts-password/passwords_tests.js index 7289de3373..9c09920905 100644 --- a/packages/accounts-password/passwords_tests.js +++ b/packages/accounts-password/passwords_tests.js @@ -204,6 +204,36 @@ if (Meteor.isServer) (function () { }); + Tinytest.add( + 'passwords - setPassword', + function (test) { + var username = Meteor.uuid(); + + var userId = Meteor.createUser({username: username}, {}); + + var user = Meteor.users.findOne(userId); + // no services yet. + test.equal(user.services.password, undefined); + + // set a new password. + Meteor.setPassword(userId, 'new password'); + user = Meteor.users.findOne(userId); + var oldVerifier = user.services.password.srp; + test.isTrue(user.services.password.srp); + + // reset with the same password, see we get a different verifier + Meteor.setPassword(userId, 'new password'); + user = Meteor.users.findOne(userId); + var newVerifier = user.services.password.srp; + test.notEqual(oldVerifier.salt, newVerifier.salt); + test.notEqual(oldVerifier.identity, newVerifier.identity); + test.notEqual(oldVerifier.verifier, newVerifier.verifier); + + // cleanup + Meteor.users.remove(userId); + }); + + // XXX would be nice to test Meteor.accounts.config({forbidSignups: true}) }) (); From 72b327f196b705c294ba623eb859b6ca5e3dad94 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 26 Sep 2012 16:55:46 -0700 Subject: [PATCH 148/239] Remove enrollAccount as a separate method and token. Just use resetPassword. --- packages/accounts-password/email_tests.js | 2 +- .../accounts-password/passwords_client.js | 26 ------------------- .../accounts-password/passwords_server.js | 26 +++---------------- packages/accounts-ui/login_buttons.js | 2 +- 4 files changed, 6 insertions(+), 50 deletions(-) diff --git a/packages/accounts-password/email_tests.js b/packages/accounts-password/email_tests.js index fbfaa5fc51..526f25b1c1 100644 --- a/packages/accounts-password/email_tests.js +++ b/packages/accounts-password/email_tests.js @@ -172,7 +172,7 @@ getEnrollAccountToken(email4, test, expect); }, function (test, expect) { - Meteor.enrollAccount(enrollAccountToken, 'password', expect(function(error) { + Meteor.resetPassword(enrollAccountToken, 'password', expect(function(error) { test.isFalse(error); })); }, diff --git a/packages/accounts-password/passwords_client.js b/packages/accounts-password/passwords_client.js index 36af1955fe..83b066042f 100644 --- a/packages/accounts-password/passwords_client.js +++ b/packages/accounts-password/passwords_client.js @@ -163,32 +163,6 @@ }); }; - // Sets a user's first password based on a token originally created by - // Meteor.enrollAccount, and then logs in the matching user. - // - // @param token {String} - // @param password {String} - // @param callback (optional) {Function(error|undefined)} - Meteor.enrollAccount = function(token, password, callback) { - if (!token) - throw new Error("Need to pass token"); - if (!password) - throw new Error("Need to pass password"); - - var verifier = Meteor._srp.generateVerifier(password); - Meteor.apply( - "enrollAccount", [token, verifier], {wait: true}, - function (error, result) { - if (error || !result) { - error = error || new Error("No result from call to enrollAccount"); - callback && callback(error); - } - - Meteor.accounts.makeClientLoggedIn(result.id, result.token); - callback && callback(); - }); - }; - // Validates a user's email address based on a token originally // created by Meteor.accounts.sendValidationEmail // diff --git a/packages/accounts-password/passwords_server.js b/packages/accounts-password/passwords_server.js index 530e73763a..5e6189ffc5 100644 --- a/packages/accounts-password/passwords_server.js +++ b/packages/accounts-password/passwords_server.js @@ -138,35 +138,17 @@ var user = Meteor.users.findOne({"services.password.reset.token": token}); if (!user) - throw new Meteor.Error(403, "Reset password link expired"); + throw new Meteor.Error(403, "Token expired"); Meteor.users.update({_id: user._id}, { $set: {'services.password.srp': newVerifier}, $unset: {'services.password.reset': 1} }); - - var loginToken = Meteor.accounts._loginTokens.insert({userId: user._id}); - this.setUserId(user._id); - return {token: loginToken, id: user._id}; - }, - - enrollAccount: function (token, newVerifier) { - if (!token) - throw new Meteor.Error(400, "Need to pass token"); - if (!newVerifier) - throw new Meteor.Error(400, "Need to pass newVerifier"); - - var user = Meteor.users.findOne({"services.password.enroll.token": token}); - if (!user) - throw new Meteor.Error(403, "Enroll account link expired"); - - Meteor.users.update({_id: user._id}, { - $set: {'services.password.srp': newVerifier}, - $unset: {'services.password.enroll': 1} - }); + // verify their email. they got the password reset email. Meteor.users.update({_id: user._id}, {$set: {"emails.0.validated": true}}); + var loginToken = Meteor.accounts._loginTokens.insert({userId: user._id}); this.setUserId(user._id); return {token: loginToken, id: user._id}; @@ -229,7 +211,7 @@ var token = Meteor.uuid(); var when = +(new Date); Meteor.users.update(userId, {$set: { - "services.password.enroll": { + "services.password.reset": { token: token, when: when } diff --git a/packages/accounts-ui/login_buttons.js b/packages/accounts-ui/login_buttons.js index 6861b73231..9aef68559c 100644 --- a/packages/accounts-ui/login_buttons.js +++ b/packages/accounts-ui/login_buttons.js @@ -424,7 +424,7 @@ if (!validatePassword(password)) return; - Meteor.enrollAccount( + Meteor.resetPassword( Session.get(ENROLL_ACCOUNT_TOKEN_KEY), password, function (error) { if (error) { From 101acac9a3818f94ed8401da0e99e18e14230b60 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 26 Sep 2012 19:51:01 -0700 Subject: [PATCH 149/239] Meteor.user() on the server (only in methods for now). --- packages/accounts-base/accounts_client.js | 6 +++- packages/accounts-base/accounts_server.js | 26 ++++++++++++++++ packages/accounts-password/passwords_tests.js | 31 ++++++++++++++++++- .../passwords_tests_setup.js | 7 +++++ 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/packages/accounts-base/accounts_client.js b/packages/accounts-base/accounts_client.js index fa7854b9e2..4d8c6c02d2 100644 --- a/packages/accounts-base/accounts_client.js +++ b/packages/accounts-base/accounts_client.js @@ -1,7 +1,11 @@ (function () { + Meteor.userId = function () { + return Meteor.default_connection.userId(); + }; + Meteor.user = function () { - var userId = Meteor.default_connection.userId(); + var userId = Meteor.userId(); if (userId) { var result = Meteor.users.findOne(userId); if (result) { diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index f6519fefd3..5215f338b6 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -73,6 +73,32 @@ }); + /// + /// CURRENT USER + /// + Meteor.userId = function () { + // This function only works if called inside a method. In theory, it + // could also be called from publish statements, since they also + // have a userId associated with them. However, given that publish + // functions aren't reactive, using any of the infomation from + // Meteor.user() in a publish function will always use the value + // from when the function first runs. This is likely not what the + // user expects. The way to make this work in a publish is to do + // Meteor.find(this.userId()).observe and recompute when the user + // record changes. + var currentInvocation = Meteor._CurrentInvocation.get(); + if (!currentInvocation || !currentInvocation.userId) + throw new Error("Meteor.userId can only be invoked in method calls."); + return currentInvocation.userId(); + }; + + Meteor.user = function () { + var userId = Meteor.userId(); + if (!userId) + return null; + return Meteor.users.findOne(userId); + }; + /// /// CREATE USER HOOKS /// diff --git a/packages/accounts-password/passwords_tests.js b/packages/accounts-password/passwords_tests.js index 9c09920905..ee8757157d 100644 --- a/packages/accounts-password/passwords_tests.js +++ b/packages/accounts-password/passwords_tests.js @@ -165,7 +165,28 @@ if (Meteor.isClient) (function () { test.equal(Meteor.user().profile.touchedByOnCreateUser, true); })); }, - logoutStep + + // test Meteor.user(). This test properly belongs in + // accounts-base/accounts_tests.js, but this is where the tests that + // actually log in are. + function(test, expect) { + var clientUser = Meteor.user(); + Meteor.call('testMeteorUser', expect(function (err, result) { + test.equal(result._id, clientUser._id); + test.equal(result.profile.touchedByOnCreateUser, true); + test.equal(err, undefined); + })); + }, + logoutStep, + function(test, expect) { + var clientUser = Meteor.user(); + test.equal(clientUser, null); + Meteor.call('testMeteorUser', expect(function (err, result) { + test.equal(err, undefined); + test.equal(result, null); + })); + } + ]); }) (); @@ -234,6 +255,14 @@ if (Meteor.isServer) (function () { }); + // This test properly belongs in accounts-base/accounts_tests.js, but + // this is where the tests that actually log in are. + Tinytest.add('accounts - user() out of context', function (test) { + // basic server context, no method. + test.throws(function () { + Meteor.user(); + }); + }); // XXX would be nice to test Meteor.accounts.config({forbidSignups: true}) }) (); diff --git a/packages/accounts-password/passwords_tests_setup.js b/packages/accounts-password/passwords_tests_setup.js index 06831bacfc..5ffeae38a6 100644 --- a/packages/accounts-password/passwords_tests_setup.js +++ b/packages/accounts-password/passwords_tests_setup.js @@ -32,3 +32,10 @@ Meteor.accounts.config({ requireEmail: false, requireUsername: false }); + + +// This test properly belongs in accounts-base/accounts_tests.js, but +// this is where the tests that actually log in are. +Meteor.methods({ + testMeteorUser: function () { return Meteor.user(); } +}); From d2616308361203215c122f962ae9a5c763b5ca09 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 26 Sep 2012 22:31:58 -0700 Subject: [PATCH 150/239] Break accounts-ui-unstyled out from accounts-ui. --- .../login_buttons.html | 0 .../login_buttons.js | 0 .../login_buttons_images.css | 0 packages/accounts-ui-unstyled/package.js | 12 ++++++++++++ packages/accounts-ui/package.js | 8 ++------ 5 files changed, 14 insertions(+), 6 deletions(-) rename packages/{accounts-ui => accounts-ui-unstyled}/login_buttons.html (100%) rename packages/{accounts-ui => accounts-ui-unstyled}/login_buttons.js (100%) rename packages/{accounts-ui => accounts-ui-unstyled}/login_buttons_images.css (100%) create mode 100644 packages/accounts-ui-unstyled/package.js diff --git a/packages/accounts-ui/login_buttons.html b/packages/accounts-ui-unstyled/login_buttons.html similarity index 100% rename from packages/accounts-ui/login_buttons.html rename to packages/accounts-ui-unstyled/login_buttons.html diff --git a/packages/accounts-ui/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js similarity index 100% rename from packages/accounts-ui/login_buttons.js rename to packages/accounts-ui-unstyled/login_buttons.js diff --git a/packages/accounts-ui/login_buttons_images.css b/packages/accounts-ui-unstyled/login_buttons_images.css similarity index 100% rename from packages/accounts-ui/login_buttons_images.css rename to packages/accounts-ui-unstyled/login_buttons_images.css diff --git a/packages/accounts-ui-unstyled/package.js b/packages/accounts-ui-unstyled/package.js new file mode 100644 index 0000000000..cca55f8501 --- /dev/null +++ b/packages/accounts-ui-unstyled/package.js @@ -0,0 +1,12 @@ +Package.describe({ + summary: "Unstyled version of login widgets" +}); + +Package.on_use(function (api) { + api.use(['accounts-urls', 'accounts-base', 'underscore', 'templating'], 'client'); + + api.add_files([ + 'login_buttons_images.css', + 'login_buttons.html', + 'login_buttons.js'], 'client'); +}); diff --git a/packages/accounts-ui/package.js b/packages/accounts-ui/package.js index 4337c58b9b..4557c75bb2 100644 --- a/packages/accounts-ui/package.js +++ b/packages/accounts-ui/package.js @@ -3,12 +3,8 @@ Package.describe({ }); Package.on_use(function (api) { - api.use(['accounts-urls', 'accounts-base', 'underscore', 'templating'], 'client'); + api.use('accounts-ui-unstyled', 'client'); api.use('less', 'server'); - api.add_files([ - 'login_buttons.less', - 'login_buttons_images.css', - 'login_buttons.html', - 'login_buttons.js'], 'client'); + api.add_files(['login_buttons.less'], 'client'); }); From c06b9ae042ae3708dd5133bcd1e60d4f442f1e9e Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 1 Oct 2012 14:45:06 -0700 Subject: [PATCH 151/239] Fix a bug in Meteor.updateOrCreateUser, and add a regression test --- packages/accounts-base/accounts_server.js | 2 +- packages/accounts-base/accounts_tests.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 5215f338b6..7ab905b86a 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -200,7 +200,7 @@ if (user) { // don't overwrite existing fields // XXX subobjects (aka 'profile', 'services')? - var newKeys = _.without(_.keys(extra), _.keys(user)); + var newKeys = _.difference(_.keys(extra), _.keys(user)); var newAttrs = _.pick(extra, newKeys); Meteor.users.update(user._id, {$set: newAttrs}); diff --git a/packages/accounts-base/accounts_tests.js b/packages/accounts-base/accounts_tests.js index 4c4d20f015..7147e9dbd9 100644 --- a/packages/accounts-base/accounts_tests.js +++ b/packages/accounts-base/accounts_tests.js @@ -12,7 +12,7 @@ Tinytest.add('accounts - updateOrCreateUser', function (test) { // create again with the same id, see that we get the same user var uid2 = Meteor.accounts.updateOrCreateUser( - {services: {facebook: {id: facebookId}}}, {bar: 2}); + {services: {facebook: {id: facebookId}}}, {foo: 1000, bar: 2}); // foo: 1000 shouldn't overwrite test.equal(uid1, uid2); test.equal(Meteor.users.find({"services.facebook.id": facebookId}).count(), 1); test.equal(Meteor.users.findOne(uid1).foo, 1); From 4c31671117dca06bfeb92aef1c00d234e3d744e3 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 1 Oct 2012 16:39:19 -0700 Subject: [PATCH 152/239] Fix default email templates to not have 'http://' in subject line. --- packages/accounts-password/email_templates.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/accounts-password/email_templates.js b/packages/accounts-password/email_templates.js index 1c56ab2e82..d5fea9fbd2 100644 --- a/packages/accounts-password/email_templates.js +++ b/packages/accounts-password/email_templates.js @@ -1,9 +1,10 @@ Meteor.accounts.emailTemplates = { from: "Meteor Accounts ", + siteName: Meteor.absoluteUrl().replace(/^https?:\/\//, '').replace(/\/$/, ''), resetPassword: { subject: function(user) { - return "How to reset your password on " + Meteor.absoluteUrl(); + return "How to reset your password on " + Meteor.accounts.emailTemplates.siteName; }, text: function(user, url) { var greeting = (user.profile && user.profile.name) ? @@ -19,7 +20,7 @@ Meteor.accounts.emailTemplates = { }, validateEmail: { subject: function(user) { - return "How to validate your account email on " + Meteor.absoluteUrl(); + return "How to validate your account email on " + Meteor.accounts.emailTemplates.siteName; }, text: function(user, url) { var greeting = (user.profile && user.profile.name) ? @@ -35,7 +36,7 @@ Meteor.accounts.emailTemplates = { }, enrollAccount: { subject: function(user) { - return "An account has been created for you on " + Meteor.absoluteUrl(); + return "An account has been created for you on " + Meteor.accounts.emailTemplates.siteName; }, text: function(user, url) { var greeting = (user.profile && user.profile.name) ? From 5fc19ef9a43bafddff36686b51876940db6ddfcc Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Tue, 2 Oct 2012 22:08:15 -0700 Subject: [PATCH 153/239] Fix redraw artifact in chrome with accounts-ui --- packages/accounts-ui-unstyled/login_buttons.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index 9aef68559c..23c45781eb 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -168,6 +168,18 @@ document.getElementById('login-email').value = usernameOrEmail; document.getElementById('login-password').value = password; + + // Forge redrawing the `login-dropdown-list` element because of + // a bizarre Chrome bug in which part of the DIV is not redrawn + // in case you had tried to unsuccessfully log in before + // switching to the signup form. + // + // Found tip on how to force a redraw on + // http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes/3485654#3485654 + var redraw = document.getElementById('login-dropdown-list'); + redraw.style.display = 'none'; + redraw.offsetHeight; // it seems that this line does nothing but is necessary for the redraw to work + redraw.style.display = 'block'; }, 'click #forgot-password-link': function () { resetMessages(); From 6cdab77381324519b40be0aff846565e21f8e547 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 1 Oct 2012 23:17:57 -0700 Subject: [PATCH 154/239] First cut at new allow/deny API. --- packages/mongo-livedata/allow_tests.js | 305 ++++++++++++++++--------- packages/mongo-livedata/collection.js | 97 ++++++-- 2 files changed, 278 insertions(+), 124 deletions(-) diff --git a/packages/mongo-livedata/allow_tests.js b/packages/mongo-livedata/allow_tests.js index 966a92e295..4316440e06 100644 --- a/packages/mongo-livedata/allow_tests.js +++ b/packages/mongo-livedata/allow_tests.js @@ -1,4 +1,9 @@ (function () { + + // + // Set up a bunch of test collections + // + // helper for defining a collection, subscribing to it, and defining // a method to clear it var defineCollection = function(name, insecure) { @@ -45,54 +50,82 @@ "collection-restrictedForUpdateOptionsTest", true /*insecure*/); var restrictedCollectionForPartialAllowTest = defineCollection( "collection-restrictedForPartialAllowTest", true /*insecure*/); + var restrictedCollectionForPartialDenyTest = defineCollection( + "collection-restrictedForPartialDenyTest", true /*insecure*/); var restrictedCollectionForFetchTest = defineCollection( "collection-restrictedForFetchTest", true /*insecure*/); var restrictedCollectionForFetchAllTest = defineCollection( "collection-restrictedForFetchAllTest", true /*insecure*/); - // two calls to allow to verify that all validators need to be - // satisfied + + // + // Set up allow/deny rules for test collections + // + + + // two calls to allow to verify that either validator is sufficient. var allows = [{ insert: function(userId, doc) { - return doc.canModify; + return doc.canInsert; }, update: function(userId, docs) { return _.all(docs, function (doc) { - return doc.canModify; + return doc.canUpdate; }); }, remove: function (userId, docs) { return _.all(docs, function (doc) { - return doc.canModify; + return doc.canRemove; }); } }, { insert: function(userId, doc) { - return doc.canInsert; + return doc.canInsert2; }, update: function(userId, docs, fields, modifier) { - return (-1 === _.indexOf(fields, 'verySecret')) && - _.all(docs, function (doc) { - return doc.canUpdate; - }); + return -1 !== _.indexOf(fields, 'canUpdate2'); }, remove: function(userId, docs) { return _.all(docs, function (doc) { - return doc.canRemove; + return doc.canRemove2; }); } }]; + // two calls to deny to verify that either one blocks the change. + var denies = [{ + insert: function(userId, doc) { + return doc.cantInsert; + }, + remove: function (userId, docs) { + return _.any(docs, function (doc) { + return doc.cantRemove; + }); + } + }, { + insert: function(userId, doc) { + return doc.cantInsert2; + }, + update: function(userId, docs, fields, modifier) { + return -1 !== _.indexOf(fields, 'verySecret'); + } + }]; + + + if (Meteor.isServer) { - _.each(allows, function (allow) { - _.each([ - restrictedCollectionDefaultSecure, - restrictedCollectionDefaultInsecure, - restrictedCollectionForUpdateOptionsTest - ], function (collection) { + _.each([ + restrictedCollectionDefaultSecure, + restrictedCollectionDefaultInsecure, + restrictedCollectionForUpdateOptionsTest + ], function (collection) { + _.each(allows, function (allow) { collection.allow(allow); }); + _.each(denies, function (deny) { + collection.deny(deny); + }); }); // just restrict one operation so that we can verify that others @@ -100,9 +133,13 @@ restrictedCollectionForPartialAllowTest.allow({ insert: function() {} }); + restrictedCollectionForPartialDenyTest.deny({ + insert: function() {} + }); + // verify that we only fetch the fields specified - we should - // be fetching just field1 and field2 + // be fetching just field1, field2, and field3. restrictedCollectionForFetchTest.allow({ insert: function() { return true; }, update: function(userId, docs) { @@ -120,6 +157,9 @@ restrictedCollectionForFetchTest.allow({ fetch: ['field2'] }); + restrictedCollectionForFetchTest.deny({ + fetch: ['field3'] + }); // verify that not passing fetch to one of the calls to allow // causes all fields to be fetched @@ -142,6 +182,11 @@ }); } + + // + // Begin actual tests + // + if (Meteor.isServer) { Tinytest.add("collection - calling allow restricts", function (test) { var collection = new Meteor.Collection(null); @@ -165,35 +210,51 @@ } ]); + // test that if allow is called once then the collection is + // restricted, and that other mutations aren't allowed + testAsyncMulti("collection - partial deny", [ + function (test, expect) { + restrictedCollectionForPartialDenyTest.update( + {world: test.runId()}, {$set: {updated: true}}, expect(function (err, res) { + test.equal(err.error, 403); + })); + } + ]); + + // test that we only fetch the fields specified testAsyncMulti("collection - fetch", [ function (test, expect) { restrictedCollectionForFetchTest.insert( - {field1: 1, field2: 1, field3: 1, world: test.runId()}); + {field1: 1, field2: 1, field3: 1, field4: 1, + world: test.runId()}); restrictedCollectionForFetchAllTest.insert( - {field1: 1, field2: 1, field3: 1, world: test.runId()}); + {field1: 1, field2: 1, field3: 1, field4: 1, + world: test.runId()}); }, function (test, expect) { restrictedCollectionForFetchTest.update( {world: test.runId()}, {$set: {updated: true}}, expect(function (err, res) { - test.equal(err.reason, "Test: Fields in doc: field1,field2,_id"); + test.equal(err.reason, + "Test: Fields in doc: field1,field2,field3,_id"); })); restrictedCollectionForFetchTest.remove( {world: test.runId()}, expect(function (err, res) { - test.equal(err.reason, "Test: Fields in doc: field1,field2,_id"); + test.equal(err.reason, + "Test: Fields in doc: field1,field2,field3,_id"); })); restrictedCollectionForFetchAllTest.update( {world: test.runId()}, {$set: {updated: true}}, expect(function (err, res) { test.equal(err.reason, - "Test: Fields in doc: field1,field2,field3,world,_id"); + "Test: Fields in doc: field1,field2,field3,field4,world,_id"); })); restrictedCollectionForFetchAllTest.remove( {world: test.runId()}, expect(function (err, res) { test.equal(err.reason, - "Test: Fields in doc: field1,field2,field3,world,_id"); + "Test: Fields in doc: field1,field2,field3,field4,world,_id"); })); } @@ -245,7 +306,7 @@ }, // put a few objects function (test, expect) { - var doc = {canInsert: true, canUpdate: true, canModify: true, world: test.runId()}; + var doc = {canInsert: true, canUpdate: true, world: test.runId()}; collection.insert(doc); collection.insert(doc); collection.insert(doc, expect(function (err, res) { @@ -274,7 +335,7 @@ } ]); }) (); - + _.each( [restrictedCollectionDefaultInsecure, restrictedCollectionDefaultSecure], function(collection) { @@ -287,78 +348,95 @@ })); }, - // insert checks validator - function (test, expect) { - collection.insert({world: test.runId(), canInsert: false}, expect(function (err, res) { - test.equal(err.error, 403); - test.equal(collection.find({world: test.runId()}).count(), 0); - })); - }, - // insert checks all validators - function (test, expect) { - collection.insert({world: test.runId(), canInsert: true}, expect(function (err, res) { - test.equal(err.error, 403); - test.equal(collection.find({world: test.runId()}).count(), 0); - })); - }, - // an insert that passes validators indeed executes + // insert with no allows passing. request is denied. function (test, expect) { collection.insert( - {canInsert: true, canModify: true, world: test.runId()}, + {world: test.runId()}, + expect(function (err, res) { + test.equal(err.error, 403); + test.equal(collection.find({world: test.runId()}).count(), 0); + })); + }, + // insert with one allow and one deny. denied. + function (test, expect) { + collection.insert( + {world: test.runId(), canInsert: true, cantInsert: true}, + expect(function (err, res) { + test.equal(err.error, 403); + test.equal(collection.find({world: test.runId()}).count(), 0); + })); + }, + // insert with one allow and other deny. denied. + function (test, expect) { + collection.insert( + {world: test.runId(), canInsert: true, cantInsert2: true}, + expect(function (err, res) { + test.equal(err.error, 403); + test.equal(collection.find({world: test.runId()}).count(), 0); + })); + }, + // insert one allow passes. allowed. + function (test, expect) { + collection.insert( + {world: test.runId(), canInsert: true}, expect(function (err, res) { test.isFalse(err); test.equal(collection.find({world: test.runId()}).count(), 1); - test.equal(collection.findOne({world: test.runId()}).canInsert, true); })); }, - // another insert executes, so that we have two different - // docs to work with (this one has canUpdate set) + // insert other allow passes. allowed. + // includes canUpdate for later. function (test, expect) { collection.insert( - {canInsert: true, canUpdate: true, canModify: true, world: test.runId()}, + {world: test.runId(), canInsert2: true, canUpdate: true}, expect(function (err, res) { test.isFalse(err); test.equal(collection.find({world: test.runId()}).count(), 2); - test.equal(collection.find({world: test.runId()}).fetch()[1].canInsert, true); - test.equal(collection.find({world: test.runId()}).fetch()[1].canUpdate, true); })); }, - // yet a third insert executes. this one has canRemove set + // yet a third insert executes. this one has canRemove and + // cantRemove set for later. function (test, expect) { collection.insert( - {canInsert: true, canRemove: true, canModify: true, world: test.runId()}, + {canInsert: true, canRemove: true, cantRemove: true, + world: test.runId()}, expect(function (err, res) { test.isFalse(err); test.equal(collection.find({world: test.runId()}).count(), 3); - test.equal(collection.find({world: test.runId()}).fetch()[1].canInsert, true); - test.equal(collection.find({world: test.runId()}).fetch()[1].canUpdate, true); - test.equal(collection.find({world: test.runId()}).fetch()[2].canInsert, true); - test.equal(collection.find({world: test.runId()}).fetch()[2].canRemove, true); })); }, // can't update to a new object function (test, expect) { collection.update( - {canInsert: true, world: test.runId()}, + {canUpdate:true, world: test.runId()}, {newObject: 1}, expect(function (err, res) { test.equal(err.error, 403); })); + // onQuiesce needed to wait for results to snap back. the + // update worked locally, and dropped the document out of + // this world. + Meteor.default_connection.onQuiesce(expect(function () { + test.equal(collection.find({world:test.runId()}).count(), 3); + })); }, - // updating dotted fields works as if we are chaninging their top part + // updating dotted fields works as if we are changing their + // top part function (test, expect) { collection.update( - {world: test.runId(), canInsert: true, canUpdate: true}, + {world: test.runId(), canUpdate: true}, {$set: {"dotted.field": 1}}, expect(function (err, res) { + test.isFalse(err); + test.equal(collection.find({world: test.runId(), canUpdate: true}).count(), 1); test.equal(collection.findOne({world: test.runId(), canUpdate: true}).dotted.field, 1); })); }, function (test, expect) { collection.update( - {world: test.runId(), canInsert: true, canUpdate: true}, + {world: test.runId(), canUpdate: true}, {$set: {"verySecret.field": 1}}, expect(function (err, res) { test.equal(err.error, 403); @@ -367,69 +445,67 @@ // update doesn't do anything if no docs match function (test, expect) { - collection.update({world: test.runId(), canInsert: false}, - {$set: {updated: true}}, expect(function (err, res) { - test.isFalse(err); - // nothing has changed - test.equal(collection.find({world: test.runId()}).count(), 3); - test.equal(collection.find({world: test.runId()}).fetch()[1].canInsert, true); - test.equal(collection.find({world: test.runId()}).fetch()[1].canUpdate, true); - test.equal(collection.find({world: test.runId()}).fetch()[1].updated, undefined); - })); + collection.update( + {world: test.runId(), doesntExist: true}, + {$set: {updated: true}}, + expect(function (err, res) { + test.isFalse(err); + // nothing has changed + test.equal(collection.find({world: test.runId()}).count(), 3); + test.equal(collection.find({world: test.runId(), updated: true}).count(), 0); + })); }, // update fails when access is denied trying to set `verySecret` function (test, expect) { - collection.update({world: test.runId(), canInsert: true}, {$set: {verySecret: true}}, expect(function (err, res) { - test.equal(err.error, 403); - // nothing has changed - test.equal(collection.find({world: test.runId()}).count(), 3); - test.equal(collection.find({world: test.runId()}).fetch()[1].canInsert, true); - test.equal(collection.find({world: test.runId()}).fetch()[1].canUpdate, true); - test.equal(collection.find({world: test.runId()}).fetch()[1].updated, undefined); - })); + collection.update( + {world: test.runId(), canUpdate: true}, + {$set: {verySecret: true}}, + expect(function (err, res) { + test.equal(err.error, 403); + // nothing has changed + test.equal(collection.find({world: test.runId()}).count(), 3); + test.equal(collection.find({world: test.runId(), updated: true}).count(), 0); + })); }, // update fails when trying to set two fields, one of which is // `verySecret` function (test, expect) { - collection.update({world: test.runId(), canInsert: true}, {$set: {updated: true, verySecret: true}}, expect(function (err, res) { - test.equal(err.error, 403); - // nothing has changed - test.equal(collection.find({world: test.runId()}).count(), 3); - test.equal(collection.find({world: test.runId()}).fetch()[1].canInsert, true); - test.equal(collection.find({world: test.runId()}).fetch()[1].canUpdate, true); - test.equal(collection.find({world: test.runId()}).fetch()[1].updated, undefined); - })); + collection.update( + {world: test.runId(), canUpdate: true}, + {$set: {updated: true, verySecret: true}}, + expect(function (err, res) { + test.equal(err.error, 403); + // nothing has changed + test.equal(collection.find({world: test.runId()}).count(), 3); + test.equal(collection.find({world: test.runId(), updated: true}).count(), 0); + })); }, // update fails when trying to modify docs that don't // have `canUpdate` set function (test, expect) { - collection.update({world: test.runId(), canInsert: true}, {$set: {updated: true}}, expect(function (err, res) { - test.equal(err.error, 403); - // nothing has changed - test.equal(collection.find({world: test.runId()}).count(), 3); - test.equal(collection.find({world: test.runId()}).fetch()[1].canInsert, true); - test.equal(collection.find({world: test.runId()}).fetch()[1].canUpdate, true); - test.equal(collection.find({world: test.runId()}).fetch()[1].updated, undefined); - })); + collection.update( + {world: test.runId(), canRemove: true}, + {$set: {updated: true}}, + expect(function (err, res) { + test.equal(err.error, 403); + // nothing has changed + test.equal(collection.find({world: test.runId()}).count(), 3); + test.equal(collection.find({world: test.runId(), updated: true}).count(), 0); + })); }, // update executes when it should function (test, expect) { - collection.update({world: test.runId(), canUpdate: true}, {$set: {updated: true}}, expect(function (err, res) { - test.isFalse(err); - test.equal(collection.find({world: test.runId()}).fetch()[1].updated, true); - })); + collection.update( + {world: test.runId(), canUpdate: true}, + {$set: {updated: true}}, + expect(function (err, res) { + test.isFalse(err); + test.equal(collection.find({world: test.runId(), updated: true}).count(), 1); + })); }, // remove fails when trying to modify an doc with no // `canRemove` set - function (test, expect) { - collection.remove({world: test.runId(), canInsert: true}, expect(function (err, res) { - test.equal(err.error, 403); - // nothing has changed - test.equal(collection.find({world: test.runId()}).count(), 3); - })); - }, - // another test that remove fails with no `canRemove` set function (test, expect) { collection.remove({world: test.runId(), canUpdate: true}, expect(function (err, res) { test.equal(err.error, 403); @@ -437,7 +513,28 @@ test.equal(collection.find({world: test.runId()}).count(), 3); })); }, - // remove executes when it should! + // remove fails when trying to modify an doc with `cantRemove` + // set + function (test, expect) { + collection.remove({world: test.runId(), canRemove: true}, expect(function (err, res) { + test.equal(err.error, 403); + // nothing has changed + test.equal(collection.find({world: test.runId()}).count(), 3); + })); + }, + + // update the doc to remove cantRemove. + function (test, expect) { + collection.update( + {world: test.runId(), canRemove: true}, + {$set: {cantRemove: false, canUpdate2: true}}, + expect(function (err, res) { + test.isFalse(err); + test.equal(collection.find({world: test.runId(), cantRemove: true}).count(), 0); + })); + }, + + // now remove can remove it. function (test, expect) { collection.remove({world: test.runId(), canRemove: true}, expect(function (err, res) { test.isFalse(err); diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 1155ec8139..c839576c80 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -130,9 +130,9 @@ Meteor.Collection.prototype._defineMutationMethods = function() { var self = this; self._validators = { - insert: [], - update: [], - remove: [], + insert: {allow: [], deny: []}, + update: {allow: [], deny: []}, + remove: {allow: [], deny: []}, fetch: [], fetchAllFields: false }; @@ -146,6 +146,8 @@ Meteor.Collection.prototype._defineMutationMethods = function() { // since tests need to check the effects of adding and removing the // `insecure` package, which sets Meteor.Collection.insecure, we // need this var + // + // XXX is this the package ordering issues? var insecure = Meteor.Collection.insecure; // mutation methods @@ -213,6 +215,8 @@ Meteor.Collection.prototype._defineMutationMethods = function() { } }; +// XXX rework doc comment +// // Restrict default mutators on collection. Can be called multiple // times, in which case all validators must be satisfied. // @@ -234,11 +238,11 @@ Meteor.Collection.prototype.allow = function(options) { self._restricted = true; if (options.insert) - self._validators.insert.push(options.insert); + self._validators.insert.allow.push(options.insert); if (options.update) - self._validators.update.push(options.update); + self._validators.update.allow.push(options.update); if (options.remove) - self._validators.remove.push(options.remove); + self._validators.remove.allow.push(options.remove); if (!self._validators.fetchAllFields) { if (options.fetch) { @@ -251,26 +255,64 @@ Meteor.Collection.prototype.allow = function(options) { } }; +Meteor.Collection.prototype.deny = function(options) { + var self = this; + self._restricted = true; + + if (options.insert) + self._validators.insert.deny.push(options.insert); + if (options.update) + self._validators.update.deny.push(options.update); + if (options.remove) + self._validators.remove.deny.push(options.remove); + + // XXX dup from allow + if (!self._validators.fetchAllFields) { + if (options.fetch) { + self._validators.fetch = _.union(self._validators.fetch, options.fetch); + } else { + self._validators.fetchAllFields = true; + // clear fetch just to make sure we don't accidentally read it + self._validators.fetch = null; + } + } +}; + + + // assuming the collection is restricted Meteor.Collection.prototype._allowInsert = function(userId, doc) { - if (this._validators.insert.length === 0) { + var self = this; + + if (self._validators.insert.allow.length === 0) { throw new Meteor.Error(403, "Access denied. No insert validators set on restricted collection."); } - // all validators should return true - return !_.any(this._validators.insert, function(validator) { - return !validator(userId, doc); - }); + // any deny returning true means access denied. + if (_.any(self._validators.insert.deny, function (validator) { + return validator(userId, doc); + })) + return false; + + // any allow returns true means allow. + if (_.any(self._validators.insert.allow, function (validator) { + return validator(userId, doc); + })) + return true; + + // otherwise, denied + return false; }; -// Simulate a mongo `update` operation while validating that the -// access control rules set by calls to `allow` are satisfied. If all +// Simulate a mongo `update` operation while validating that the access +// control rules set by calls to `allow/deny` are satisfied. If all // pass, rewrite the mongo operation to use $in to set the list of // document ids to change ##ValidatedChange Meteor.Collection.prototype._validatedUpdate = function(userId, selector, mutator, options) { var self = this; - if (self._validators.update.length === 0) { + // short circuit. If no allows are set, we know this won't be allowed. + if (self._validators.update.allow.length === 0) { throw new Meteor.Error(403, "Access denied. No update validators set on restricted collection."); } @@ -306,13 +348,20 @@ Meteor.Collection.prototype._validatedUpdate = function(userId, selector, mutato docs = self._collection.find(selector, findOptions).fetch(); } else { var doc = self._collection.findOne(selector, findOptions); - if (!doc) // none satisfied! + if (!doc) // none satisfied! return; docs = [doc]; } - // verify that all validators return true - if (_.any(self._validators.update, function(validator) { + // call user validators. + // Any deny returns true means denied. + if (_.any(self._validators.update.deny, function(validator) { + return validator(userId, docs, fields, mutator); + })) { + throw new Meteor.Error(403, "Access denied"); + } + // Any allow returns true means proceed. Throw error if they all fail. + if (_.all(self._validators.update.allow, function(validator) { return !validator(userId, docs, fields, mutator); })) { throw new Meteor.Error(403, "Access denied"); @@ -337,7 +386,8 @@ Meteor.Collection.prototype._validatedUpdate = function(userId, selector, mutato Meteor.Collection.prototype._validatedRemove = function(userId, selector) { var self = this; - if (self._validators.remove.length === 0) { + // short circuit if there is no way it will pass. + if (self._validators.remove.allow.length === 0) { throw new Meteor.Error(403, "Access denied. No remove validators set on restricted collection."); } @@ -351,8 +401,15 @@ Meteor.Collection.prototype._validatedRemove = function(userId, selector) { var docs = self._collection.find(selector, findOptions).fetch(); - // verify that all validators return true - if (_.any(self._validators.remove, function(validator) { + // call user validators. + // Any deny returns true means denied. + if (_.any(self._validators.remove.deny, function(validator) { + return validator(userId, docs); + })) { + throw new Meteor.Error(403, "Access denied"); + } + // Any allow returns true means proceed. Throw error if they all fail. + if (_.all(self._validators.remove.allow, function(validator) { return !validator(userId, docs); })) { throw new Meteor.Error(403, "Access denied"); From 9fe341e3710385427089aa557a112376d76db2f5 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 2 Oct 2012 17:18:20 -0700 Subject: [PATCH 155/239] Reorder file. No functional changes. --- packages/mongo-livedata/collection.js | 327 +++++++++++++------------- 1 file changed, 169 insertions(+), 158 deletions(-) diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index c839576c80..21c3128dab 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -102,6 +102,11 @@ Meteor.Collection = function (name, manager, driver, preventAutopublish) { }); }; +/// +/// Main collection API +/// + + _.extend(Meteor.Collection.prototype, { find: function (/* selector, options */) { // Collection.find() (return all docs) behaves differently @@ -126,6 +131,170 @@ _.extend(Meteor.Collection.prototype, { }); + +// 'insert' immediately returns the inserted document's new _id. The +// others return nothing. +// +// Otherwise, the semantics are exactly like other methods: they take +// a callback as an optional last argument; if no callback is +// provided, they block until the operation is complete, and throw an +// exception if it fails; if a callback is provided, then they don't +// necessarily block, and they call the callback when they finish with +// error and result arguments. (The insert method provides the +// document ID as its result; update and remove don't provide a result.) +// +// On the client, blocking is impossible, so if a callback +// isn't provided, they just return immediately and any error +// information is lost. +// +// There's one more tweak. On the client, if you don't provide a +// callback, then if there is an error, a message will be logged with +// Meteor._debug. +// +// The intent (though this is actually determined by the underlying +// drivers) is that the operations should be done synchronously, not +// generating their result until the database has acknowledged +// them. In the future maybe we should provide a flag to turn this +// off. +_.each(["insert", "update", "remove"], function (name) { + Meteor.Collection.prototype[name] = function (/* arguments */) { + var self = this; + var args = _.toArray(arguments); + var callback; + var ret; + + if (args.length && args[args.length - 1] instanceof Function) + callback = args.pop(); + + if (Meteor.isClient && !callback) { + // Client can't block, so it can't report errors by exception, + // only by callback. If they forget the callback, give them a + // default one that logs the error, so they aren't totally + // baffled if their writes don't work because their database is + // down. + callback = function (err) { + if (err) + Meteor._debug(name + " failed: " + err.error + " -- " + err.reason); + }; + } + + if (name === "insert") { + if (!args.length) + throw new Error("insert requires an argument"); + // shallow-copy the document and generate an ID + args[0] = _.extend({}, args[0]); + if ('_id' in args[0]) + throw new Error("Do not pass an _id to insert. Meteor will generate the _id for you."); + ret = args[0]._id = Meteor.uuid(); + } + + if (self._manager && self._manager !== Meteor.default_server) { + // just remote to another endpoint, propagate return value or + // exception. + if (callback) { + // asynchronous: on success, callback should return ret + // (document ID for insert, undefined for update and + // remove), not the method's result. + self._manager.apply(self._prefix + name, args, function (error, result) { + callback(error, !error && ret); + }); + } else { + // synchronous: propagate exception + self._manager.apply(self._prefix + name, args); + } + + } else { + // it's my collection. descend into the collection object + // and propagate any exception. + try { + self._collection[name].apply(self._collection, args); + } catch (e) { + if (callback) { + callback(e); + return null; + } + throw e; + } + + // on success, return *ret*, not the manager's return value. + callback && callback(null, ret); + } + + // both sync and async, unless we threw an exception, return ret + // (new document ID for insert, undefined otherwise). + return ret; + }; +}); + + +/// +/// Remote methods and access control. +/// + +// XXX rework doc comment +// +// Restrict default mutators on collection. Can be called multiple +// times, in which case all validators must be satisfied. +// +// options.insert {Function(userId, doc)} +// return true to allow the user to add this document +// +// options.update {Function(userId, docs, fields, modifier)} +// return true to allow the user to update these documents. +// `fields` is passed as an array of fields that are to be modified +// +// options.remove {Function(userId, docs)} +// return true to allow the user to remove these documents +// +// options.fetch {Array} +// Fields to fetch for these validators. If any call to allow does +// not have this option then all fields are loaded. +Meteor.Collection.prototype.allow = function(options) { + var self = this; + self._restricted = true; + + if (options.insert) + self._validators.insert.allow.push(options.insert); + if (options.update) + self._validators.update.allow.push(options.update); + if (options.remove) + self._validators.remove.allow.push(options.remove); + + if (!self._validators.fetchAllFields) { + if (options.fetch) { + self._validators.fetch = _.union(self._validators.fetch, options.fetch); + } else { + self._validators.fetchAllFields = true; + // clear fetch just to make sure we don't accidentally read it + self._validators.fetch = null; + } + } +}; + +Meteor.Collection.prototype.deny = function(options) { + var self = this; + self._restricted = true; + + if (options.insert) + self._validators.insert.deny.push(options.insert); + if (options.update) + self._validators.update.deny.push(options.update); + if (options.remove) + self._validators.remove.deny.push(options.remove); + + // XXX dup from allow + if (!self._validators.fetchAllFields) { + if (options.fetch) { + self._validators.fetch = _.union(self._validators.fetch, options.fetch); + } else { + self._validators.fetchAllFields = true; + // clear fetch just to make sure we don't accidentally read it + self._validators.fetch = null; + } + } +}; + + Meteor.Collection.prototype._defineMutationMethods = function() { var self = this; @@ -215,70 +384,6 @@ Meteor.Collection.prototype._defineMutationMethods = function() { } }; -// XXX rework doc comment -// -// Restrict default mutators on collection. Can be called multiple -// times, in which case all validators must be satisfied. -// -// options.insert {Function(userId, doc)} -// return true to allow the user to add this document -// -// options.update {Function(userId, docs, fields, modifier)} -// return true to allow the user to update these documents. -// `fields` is passed as an array of fields that are to be modified -// -// options.remove {Function(userId, docs)} -// return true to allow the user to remove these documents -// -// options.fetch {Array} -// Fields to fetch for these validators. If any call to allow does -// not have this option then all fields are loaded. -Meteor.Collection.prototype.allow = function(options) { - var self = this; - self._restricted = true; - - if (options.insert) - self._validators.insert.allow.push(options.insert); - if (options.update) - self._validators.update.allow.push(options.update); - if (options.remove) - self._validators.remove.allow.push(options.remove); - - if (!self._validators.fetchAllFields) { - if (options.fetch) { - self._validators.fetch = _.union(self._validators.fetch, options.fetch); - } else { - self._validators.fetchAllFields = true; - // clear fetch just to make sure we don't accidentally read it - self._validators.fetch = null; - } - } -}; - -Meteor.Collection.prototype.deny = function(options) { - var self = this; - self._restricted = true; - - if (options.insert) - self._validators.insert.deny.push(options.insert); - if (options.update) - self._validators.update.deny.push(options.update); - if (options.remove) - self._validators.remove.deny.push(options.remove); - - // XXX dup from allow - if (!self._validators.fetchAllFields) { - if (options.fetch) { - self._validators.fetch = _.union(self._validators.fetch, options.fetch); - } else { - self._validators.fetchAllFields = true; - // clear fetch just to make sure we don't accidentally read it - self._validators.fetch = null; - } - } -}; - - // assuming the collection is restricted Meteor.Collection.prototype._allowInsert = function(userId, doc) { @@ -424,97 +529,3 @@ Meteor.Collection.prototype._validatedRemove = function(userId, selector) { self._collection.remove.call(self._collection, idSelector); }; - -// 'insert' immediately returns the inserted document's new _id. The -// others return nothing. -// -// Otherwise, the semantics are exactly like other methods: they take -// a callback as an optional last argument; if no callback is -// provided, they block until the operation is complete, and throw an -// exception if it fails; if a callback is provided, then they don't -// necessarily block, and they call the callback when they finish with -// error and result arguments. (The insert method provides the -// document ID as its result; update and remove don't provide a result.) -// -// On the client, blocking is impossible, so if a callback -// isn't provided, they just return immediately and any error -// information is lost. -// -// There's one more tweak. On the client, if you don't provide a -// callback, then if there is an error, a message will be logged with -// Meteor._debug. -// -// The intent (though this is actually determined by the underlying -// drivers) is that the operations should be done synchronously, not -// generating their result until the database has acknowledged -// them. In the future maybe we should provide a flag to turn this -// off. -_.each(["insert", "update", "remove"], function (name) { - Meteor.Collection.prototype[name] = function (/* arguments */) { - var self = this; - var args = _.toArray(arguments); - var callback; - var ret; - - if (args.length && args[args.length - 1] instanceof Function) - callback = args.pop(); - - if (Meteor.isClient && !callback) { - // Client can't block, so it can't report errors by exception, - // only by callback. If they forget the callback, give them a - // default one that logs the error, so they aren't totally - // baffled if their writes don't work because their database is - // down. - callback = function (err) { - if (err) - Meteor._debug(name + " failed: " + err.error + " -- " + err.reason); - }; - } - - if (name === "insert") { - if (!args.length) - throw new Error("insert requires an argument"); - // shallow-copy the document and generate an ID - args[0] = _.extend({}, args[0]); - if ('_id' in args[0]) - throw new Error("Do not pass an _id to insert. Meteor will generate the _id for you."); - ret = args[0]._id = Meteor.uuid(); - } - - if (self._manager && self._manager !== Meteor.default_server) { - // just remote to another endpoint, propagate return value or - // exception. - if (callback) { - // asynchronous: on success, callback should return ret - // (document ID for insert, undefined for update and - // remove), not the method's result. - self._manager.apply(self._prefix + name, args, function (error, result) { - callback(error, !error && ret); - }); - } else { - // synchronous: propagate exception - self._manager.apply(self._prefix + name, args); - } - - } else { - // it's my collection. descend into the collection object - // and propagate any exception. - try { - self._collection[name].apply(self._collection, args); - } catch (e) { - if (callback) { - callback(e); - return null; - } - throw e; - } - - // on success, return *ret*, not the manager's return value. - callback && callback(null, ret); - } - - // both sync and async, unless we threw an exception, return ret - // (new document ID for insert, undefined otherwise). - return ret; - }; -}); From 290dc70766043f2b71d964db78f09319e16a7e5e Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 2 Oct 2012 22:08:30 -0700 Subject: [PATCH 156/239] Stop autopublish warning when use accounts. --- packages/accounts-base/accounts_server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 7ab905b86a..c93c79273d 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -243,7 +243,7 @@ // Publish all login service configuration fields other than secret. Meteor.publish("loginServiceConfiguration", function () { return Meteor.accounts.configuration.find({}, {fields: {secret: 0}}); - }); + }, {is_auto: true}); // not techincally autopublish, but stops the warning. // Allow a one-time configuration for a login service. Meteor.accounts.configuration.allow({}); // disallow mutators From e5bf91fc24c37b0e716f3b7c36a65f9db5ba81f7 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 2 Oct 2012 17:21:17 -0700 Subject: [PATCH 157/239] Use the current state of Meteor.Collection.insecure, not the state when the collection was defined. --- packages/mongo-livedata/allow_tests.js | 26 +++++++++-- packages/mongo-livedata/collection.js | 65 ++++++++++++++------------ 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/packages/mongo-livedata/allow_tests.js b/packages/mongo-livedata/allow_tests.js index 4316440e06..f5b5a1cf10 100644 --- a/packages/mongo-livedata/allow_tests.js +++ b/packages/mongo-livedata/allow_tests.js @@ -7,10 +7,8 @@ // helper for defining a collection, subscribing to it, and defining // a method to clear it var defineCollection = function(name, insecure) { - var oldInsecure = Meteor.Collection.insecure; - Meteor.Collection.insecure = insecure; var collection = new Meteor.Collection(name); - Meteor.Collection.insecure = oldInsecure; + collection._insecure = insecure; if (Meteor.isServer) { Meteor.publish("collection-" + name, function() { @@ -190,12 +188,32 @@ if (Meteor.isServer) { Tinytest.add("collection - calling allow restricts", function (test) { var collection = new Meteor.Collection(null); - test.equal(collection._restricted, undefined); + test.equal(collection._restricted, false); collection.allow({ insert: function() {} }); test.equal(collection._restricted, true); }); + + Tinytest.add("collection - global insecure", function (test) { + // note: This test alters the global insecure status! This may + // collide with itself if run multiple times (but is better than + // the old test which had the same problem) + var oldGlobalInsecure = Meteor.Collection.insecure; + + Meteor.Collection.insecure = true; + var collection = new Meteor.Collection(null); + test.equal(collection._isInsecure(), true); + + Meteor.Collection.insecure = false; + test.equal(collection._isInsecure(), false); + + collection._insecure = true; + test.equal(collection._isInsecure(), true); + + Meteor.Collection.insecure = oldGlobalInsecure; + }); + } if (Meteor.isClient) { diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 21c3128dab..ac2a7a5e3c 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -260,15 +260,7 @@ Meteor.Collection.prototype.allow = function(options) { if (options.remove) self._validators.remove.allow.push(options.remove); - if (!self._validators.fetchAllFields) { - if (options.fetch) { - self._validators.fetch = _.union(self._validators.fetch, options.fetch); - } else { - self._validators.fetchAllFields = true; - // clear fetch just to make sure we don't accidentally read it - self._validators.fetch = null; - } - } + self._updateFetch(options.fetch); }; Meteor.Collection.prototype.deny = function(options) { @@ -282,22 +274,23 @@ Meteor.Collection.prototype.deny = function(options) { if (options.remove) self._validators.remove.deny.push(options.remove); - // XXX dup from allow - if (!self._validators.fetchAllFields) { - if (options.fetch) { - self._validators.fetch = _.union(self._validators.fetch, options.fetch); - } else { - self._validators.fetchAllFields = true; - // clear fetch just to make sure we don't accidentally read it - self._validators.fetch = null; - } - } + self._updateFetch(options.fetch); }; Meteor.Collection.prototype._defineMutationMethods = function() { var self = this; + // set to true once we call any allow or deny methods. If true, use + // allow/deny semanitcs. If false, use insecure mode semanitcs. + self._restricted = false; + + // Insecure mode (default to allowing writes). Defaults to 'undefined' + // which means use the global Meteor.Collection.insecure. This + // property can be overriden by tests or packages wishing to change + // insecure mode behavior of their collections. + self._insecure = undefined; + self._validators = { insert: {allow: [], deny: []}, update: {allow: [], deny: []}, @@ -312,13 +305,6 @@ Meteor.Collection.prototype._defineMutationMethods = function() { // XXX what if name has illegal characters in it? self._prefix = '/' + self._name + '/'; - // since tests need to check the effects of adding and removing the - // `insecure` package, which sets Meteor.Collection.insecure, we - // need this var - // - // XXX is this the package ordering issues? - var insecure = Meteor.Collection.insecure; - // mutation methods if (self._manager) { var m = {}; @@ -331,7 +317,7 @@ Meteor.Collection.prototype._defineMutationMethods = function() { if (!self._allowInsert(this.userId(), doc)) throw new Meteor.Error(403, "Access denied"); } else { - if (!insecure) + if (!self._isInsecure()) throw new Meteor.Error(403, "Access denied"); } } @@ -350,7 +336,7 @@ Meteor.Collection.prototype._defineMutationMethods = function() { if (self._restricted) { self._validatedUpdate(this.userId(), selector, mutator, options); } else { - if (insecure) { + if (self._isInsecure()) { // update returns nothing. allow exceptions to propagate. self._collection.update(selector, mutator, options); } else { @@ -370,7 +356,7 @@ Meteor.Collection.prototype._defineMutationMethods = function() { if (self._restricted) { self._validatedRemove(this.userId(), selector); } else { - if (insecure) { + if (self._isInsecure()) { // insert returns nothing. allow exceptions to propagate. self._collection.remove(selector); } else { @@ -385,6 +371,27 @@ Meteor.Collection.prototype._defineMutationMethods = function() { }; +Meteor.Collection.prototype._updateFetch = function (fields) { + var self = this; + + if (!self._validators.fetchAllFields) { + if (fields) { + self._validators.fetch = _.union(self._validators.fetch, fields); + } else { + self._validators.fetchAllFields = true; + // clear fetch just to make sure we don't accidentally read it + self._validators.fetch = null; + } + } +}; + +Meteor.Collection.prototype._isInsecure = function () { + var self = this; + if (self._insecure === undefined) + return Meteor.Collection.insecure; + return self._insecure; +}; + // assuming the collection is restricted Meteor.Collection.prototype._allowInsert = function(userId, doc) { var self = this; From d759f93e5754d27a9cfe5b6b86f4a71e6b850165 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 2 Oct 2012 17:39:19 -0700 Subject: [PATCH 158/239] Refactor insert to be parallel to update and remove. Should have no functional impact. --- packages/mongo-livedata/collection.js | 80 +++++++++++---------------- 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index ac2a7a5e3c..584a34df7f 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -312,37 +312,28 @@ Meteor.Collection.prototype._defineMutationMethods = function() { m[self._prefix + 'insert'] = function (doc) { self._maybe_snapshot(); - if (!this.isSimulation) { - if (self._restricted) { - if (!self._allowInsert(this.userId(), doc)) - throw new Meteor.Error(403, "Access denied"); - } else { - if (!self._isInsecure()) - throw new Meteor.Error(403, "Access denied"); - } + if (this.isSimulation) { + self._collection.insert(doc); + } else if (self._restricted) { + self._validatedInsert(this.userId(), doc); + } else if (self._isInsecure()) { + self._collection.insert(doc); + } else { + throw new Meteor.Error(403, "Access denied"); } - - // insert returns nothing. allow exceptions to propagate. - self._collection.insert(doc); }; m[self._prefix + 'update'] = function (selector, mutator, options) { self._maybe_snapshot(); if (this.isSimulation) { - // insert returns nothing. allow exceptions to propagate. + self._collection.update(selector, mutator, options); + } else if (self._restricted) { + self._validatedUpdate(this.userId(), selector, mutator, options); + } else if (self._isInsecure()) { self._collection.update(selector, mutator, options); } else { - if (self._restricted) { - self._validatedUpdate(this.userId(), selector, mutator, options); - } else { - if (self._isInsecure()) { - // update returns nothing. allow exceptions to propagate. - self._collection.update(selector, mutator, options); - } else { - throw new Meteor.Error(403, "Access denied"); - } - } + throw new Meteor.Error(403, "Access denied"); } }; @@ -350,19 +341,13 @@ Meteor.Collection.prototype._defineMutationMethods = function() { self._maybe_snapshot(); if (this.isSimulation) { - // remove returns nothing. allow exceptions to propagate. + self._collection.remove(selector); + } else if (self._restricted) { + self._validatedRemove(this.userId(), selector); + } else if (self._isInsecure()) { self._collection.remove(selector); } else { - if (self._restricted) { - self._validatedRemove(this.userId(), selector); - } else { - if (self._isInsecure()) { - // insert returns nothing. allow exceptions to propagate. - self._collection.remove(selector); - } else { - throw new Meteor.Error(403, "Access denied"); - } - } + throw new Meteor.Error(403, "Access denied"); } }; @@ -392,28 +377,29 @@ Meteor.Collection.prototype._isInsecure = function () { return self._insecure; }; -// assuming the collection is restricted -Meteor.Collection.prototype._allowInsert = function(userId, doc) { +Meteor.Collection.prototype._validatedInsert = function(userId, doc) { var self = this; + // short circuit if there is no way it will pass. if (self._validators.insert.allow.length === 0) { throw new Meteor.Error(403, "Access denied. No insert validators set on restricted collection."); } - // any deny returning true means access denied. - if (_.any(self._validators.insert.deny, function (validator) { + // call user validators. + // Any deny returns true means denied. + if (_.any(self._validators.insert.deny, function(validator) { return validator(userId, doc); - })) - return false; + })) { + throw new Meteor.Error(403, "Access denied"); + } + // Any allow returns true means proceed. Throw error if they all fail. + if (_.all(self._validators.insert.allow, function(validator) { + return !validator(userId, doc); + })) { + throw new Meteor.Error(403, "Access denied"); + } - // any allow returns true means allow. - if (_.any(self._validators.insert.allow, function (validator) { - return validator(userId, doc); - })) - return true; - - // otherwise, denied - return false; + self._collection.insert.call(self._collection, doc); }; // Simulate a mongo `update` operation while validating that the access From b2801f4c028779705978eb999e038ff1c53d8020 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 2 Oct 2012 20:04:37 -0700 Subject: [PATCH 159/239] Make sure to lock down sensitive accounts collections, even in insecure mode. Don't update fetch fields when we don't ask for any fetching. Rework comments. --- packages/accounts-base/accounts_common.js | 16 +++++++++ .../accounts-password/passwords_common.js | 7 ---- .../accounts-password/passwords_server.js | 16 +++++++++ packages/mongo-livedata/allow_tests.js | 2 +- packages/mongo-livedata/collection.js | 33 ++++++++++++------- 5 files changed, 55 insertions(+), 19 deletions(-) diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index 68f61e67cc..cee3fb010b 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -22,6 +22,13 @@ Meteor.accounts._loginTokens = new Meteor.Collection( null /*manager*/, null /*driver*/, true /*preventAutopublish*/); +// Don't let people write to the collection, even in insecure +// mode. There's no good reason for people to be fishing around in this +// table, and it is _really_ insecure to allow it as users could easily +// steal sessions and impersonate other users. Users can override by +// calling more allows later, if they really want. +Meteor.accounts._loginTokens.allow({}); + // Users table. Don't use the normal autopublish, since we want to hide // some fields. Code to autopublish this is in accounts_server.js. @@ -30,6 +37,9 @@ Meteor.users = new Meteor.Collection( null /*manager*/, null /*driver*/, true /*preventAutopublish*/); +// There is an allow call in accounts_server that restricts this +// collection. + // Table containing documents with configuration options for each // login service @@ -38,6 +48,12 @@ Meteor.accounts.configuration = new Meteor.Collection( null /*manager*/, null /*driver*/, true /*preventAutopublish*/); +// Leave this collection open in insecure mode. In theory, someone could +// hijack your oauth connect requests to a different endpoint or appId, +// but you did ask for 'insecure'. The advantage is that it is much +// easier to write a configuration wizard that works only in insecure +// mode. + // Thrown when trying to use a login service which is not configured Meteor.accounts.ConfigError = function(description) { diff --git a/packages/accounts-password/passwords_common.js b/packages/accounts-password/passwords_common.js index 24dc1f2bd6..1bf1351dae 100644 --- a/packages/accounts-password/passwords_common.js +++ b/packages/accounts-password/passwords_common.js @@ -1,8 +1 @@ Meteor.accounts.passwords = {}; - -// internal email validation tokens collection. Never published. -Meteor.accounts._emailValidationTokens = new Meteor.Collection( - "accounts._emailValidationTokens", - null /*manager*/, - null /*driver*/, - true /*preventAutopublish*/); \ No newline at end of file diff --git a/packages/accounts-password/passwords_server.js b/packages/accounts-password/passwords_server.js index 5e6189ffc5..2cba4db4e1 100644 --- a/packages/accounts-password/passwords_server.js +++ b/packages/accounts-password/passwords_server.js @@ -6,6 +6,22 @@ null /*manager*/, null /*driver*/, true /*preventAutopublish*/); + // Don't let people write to the collection, even in insecure + // mode. There's no good reason for people to be fishing around in this + // table, and it is _really_ insecure to allow it as users could easily + // steal sessions and impersonate other users. Users can override by + // calling more allows later, if they really want. + Meteor.accounts._srpChallenges.allow({}); + + // internal email validation tokens collection. Never published. + Meteor.accounts._emailValidationTokens = new Meteor.Collection( + "accounts._emailValidationTokens", + null /*manager*/, + null /*driver*/, + true /*preventAutopublish*/); + // also lock down email validation. These can be used to log in. + Meteor.accounts._emailValidationTokens.allow({}); + var selectorFromUserQuery = function (user) { if (!user) diff --git a/packages/mongo-livedata/allow_tests.js b/packages/mongo-livedata/allow_tests.js index f5b5a1cf10..a8ed65d1b1 100644 --- a/packages/mongo-livedata/allow_tests.js +++ b/packages/mongo-livedata/allow_tests.js @@ -176,7 +176,7 @@ fetch: ['field1'] }); restrictedCollectionForFetchAllTest.allow({ - insert: function() { return true; } + update: function() { return true; } }); } diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 584a34df7f..f75e43dea5 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -231,24 +231,29 @@ _.each(["insert", "update", "remove"], function (name) { /// Remote methods and access control. /// -// XXX rework doc comment -// -// Restrict default mutators on collection. Can be called multiple -// times, in which case all validators must be satisfied. +// Restrict default mutators on collection. allow() and deny() take the +// same options: // // options.insert {Function(userId, doc)} -// return true to allow the user to add this document +// return true to allow/deny adding this document // // options.update {Function(userId, docs, fields, modifier)} -// return true to allow the user to update these documents. +// return true to allow/deny updating these documents. // `fields` is passed as an array of fields that are to be modified // // options.remove {Function(userId, docs)} -// return true to allow the user to remove these documents +// return true to allow/deny removing these documents // // options.fetch {Array} -// Fields to fetch for these validators. If any call to allow does -// not have this option then all fields are loaded. +// Fields to fetch for these validators. If any call to allow or deny +// does not have this option then all fields are loaded. +// +// allow and deny can be called multiple times. The validators are +// evaluated as follows: +// 1) If any deny() function returns true, the request is denied. +// 2) If any allow() function returns true, the requested is allowed. +// 3) The request is denied. + Meteor.Collection.prototype.allow = function(options) { var self = this; self._restricted = true; @@ -260,7 +265,11 @@ Meteor.Collection.prototype.allow = function(options) { if (options.remove) self._validators.remove.allow.push(options.remove); - self._updateFetch(options.fetch); + // Only update the fetch fields if we're passed things that affect + // fetching. This way allow({}) doesn't result in setting + // fetchAllFields + if (options.update || options.remove || options.fetch) + self._updateFetch(options.fetch); }; Meteor.Collection.prototype.deny = function(options) { @@ -274,7 +283,9 @@ Meteor.Collection.prototype.deny = function(options) { if (options.remove) self._validators.remove.deny.push(options.remove); - self._updateFetch(options.fetch); + // same as allow. see above. + if (options.update || options.remove || options.fetch) + self._updateFetch(options.fetch); }; From 0b1c7d10b0793198b38b51f17faf775bfe3bdf42 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 2 Oct 2012 21:31:45 -0700 Subject: [PATCH 160/239] Feedback from review. --- packages/accounts-base/accounts_server.js | 3 --- packages/mongo-livedata/allow_tests.js | 2 +- packages/mongo-livedata/collection.js | 12 ++++++------ 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index c93c79273d..90d152623f 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -262,9 +262,6 @@ /// Meteor.users.allow({ - // clients can't insert or remove users - insert: function () { return false; }, - remove: function () { return false; }, // clients can modify the profile field of their own document, and // nothing else. update: function (userId, docs, fields, modifier) { diff --git a/packages/mongo-livedata/allow_tests.js b/packages/mongo-livedata/allow_tests.js index a8ed65d1b1..feed2b0055 100644 --- a/packages/mongo-livedata/allow_tests.js +++ b/packages/mongo-livedata/allow_tests.js @@ -228,7 +228,7 @@ } ]); - // test that if allow is called once then the collection is + // test that if deny is called once then the collection is // restricted, and that other mutations aren't allowed testAsyncMulti("collection - partial deny", [ function (test, expect) { diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index f75e43dea5..ca002967f2 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -250,9 +250,9 @@ _.each(["insert", "update", "remove"], function (name) { // // allow and deny can be called multiple times. The validators are // evaluated as follows: -// 1) If any deny() function returns true, the request is denied. -// 2) If any allow() function returns true, the requested is allowed. -// 3) The request is denied. +// - If any deny() function returns true, the request is denied. +// - Otherwise, if any allow() function returns true, the requested is allowed. +// - Otherwise, the request is denied. Meteor.Collection.prototype.allow = function(options) { var self = this; @@ -393,7 +393,7 @@ Meteor.Collection.prototype._validatedInsert = function(userId, doc) { // short circuit if there is no way it will pass. if (self._validators.insert.allow.length === 0) { - throw new Meteor.Error(403, "Access denied. No insert validators set on restricted collection."); + throw new Meteor.Error(403, "Access denied. No allow validators set on restricted collection."); } // call user validators. @@ -422,7 +422,7 @@ Meteor.Collection.prototype._validatedUpdate = function(userId, selector, mutato // short circuit. If no allows are set, we know this won't be allowed. if (self._validators.update.allow.length === 0) { - throw new Meteor.Error(403, "Access denied. No update validators set on restricted collection."); + throw new Meteor.Error(403, "Access denied. No allow validators set on restricted collection."); } // compute modified fields @@ -497,7 +497,7 @@ Meteor.Collection.prototype._validatedRemove = function(userId, selector) { // short circuit if there is no way it will pass. if (self._validators.remove.allow.length === 0) { - throw new Meteor.Error(403, "Access denied. No remove validators set on restricted collection."); + throw new Meteor.Error(403, "Access denied. No allow validators set on restricted collection."); } var findOptions = {}; From 1eceed28f9e866a23decf5c76ba25889ef90aa6a Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 1 Oct 2012 13:44:14 -0700 Subject: [PATCH 161/239] Replace 'Meteor.accounts' with 'Accounts'. find . -name '*.js' -print0 | xargs -0 perl -pi -e 's/Meteor\.accounts/Accounts/g' --- examples/todos/accounts/config.js | 2 +- examples/todos/accounts/server/secrets.js | 4 +- examples/todos/accounts/services.js | 4 +- packages/accounts-base/accounts_client.js | 4 +- packages/accounts-base/accounts_common.js | 30 +++---- packages/accounts-base/accounts_server.js | 34 ++++---- packages/accounts-base/accounts_tests.js | 22 ++--- packages/accounts-base/localstorage_token.js | 52 ++++++------ packages/accounts-facebook/facebook_client.js | 12 +-- packages/accounts-facebook/facebook_common.js | 8 +- packages/accounts-facebook/facebook_server.js | 6 +- packages/accounts-google/google_client.js | 12 +-- packages/accounts-google/google_common.js | 8 +- packages/accounts-google/google_server.js | 10 +-- .../accounts-oauth-helper/oauth_client.js | 6 +- .../accounts-oauth-helper/oauth_common.js | 2 +- .../accounts-oauth-helper/oauth_server.js | 36 ++++---- .../accounts-oauth1-helper/oauth1_common.js | 2 +- .../accounts-oauth1-helper/oauth1_server.js | 24 +++--- .../accounts-oauth1-helper/oauth1_tests.js | 40 ++++----- .../accounts-oauth2-helper/oauth2_common.js | 2 +- .../accounts-oauth2-helper/oauth2_server.js | 10 +-- .../accounts-oauth2-helper/oauth2_tests.js | 36 ++++---- packages/accounts-password/email_templates.js | 8 +- .../accounts-password/email_tests_setup.js | 2 +- .../accounts-password/passwords_client.js | 8 +- .../accounts-password/passwords_common.js | 2 +- .../accounts-password/passwords_server.js | 76 ++++++++--------- packages/accounts-password/passwords_tests.js | 12 +-- .../passwords_tests_setup.js | 6 +- packages/accounts-twitter/twitter_client.js | 6 +- packages/accounts-twitter/twitter_common.js | 6 +- packages/accounts-twitter/twitter_server.js | 2 +- .../accounts-ui-unstyled/login_buttons.js | 82 +++++++++---------- packages/accounts-urls/url_client.js | 16 ++-- packages/accounts-urls/url_server.js | 14 ++-- packages/accounts-weibo/weibo_client.js | 6 +- packages/accounts-weibo/weibo_common.js | 12 +-- packages/accounts-weibo/weibo_server.js | 6 +- 39 files changed, 315 insertions(+), 315 deletions(-) diff --git a/examples/todos/accounts/config.js b/examples/todos/accounts/config.js index 55cd07f40f..cde224a4dd 100644 --- a/examples/todos/accounts/config.js +++ b/examples/todos/accounts/config.js @@ -1,4 +1,4 @@ -Meteor.accounts.config({ +Accounts.config({ requireEmail: false, requireUsername: false, validateEmails: true diff --git a/examples/todos/accounts/server/secrets.js b/examples/todos/accounts/server/secrets.js index f1463cdeff..86792b48ff 100644 --- a/examples/todos/accounts/server/secrets.js +++ b/examples/todos/accounts/server/secrets.js @@ -1,5 +1,5 @@ // Modify and uncomment the following lines to configure login services. // Also see accounts/services.js -// Meteor.accounts.facebook.setSecret('SECRET'); -// Meteor.accounts.google.setSecret('SECRET'); +// Accounts.facebook.setSecret('SECRET'); +// Accounts.google.setSecret('SECRET'); diff --git a/examples/todos/accounts/services.js b/examples/todos/accounts/services.js index 1ab4e5d6c2..8e2ad94ddb 100644 --- a/examples/todos/accounts/services.js +++ b/examples/todos/accounts/services.js @@ -1,5 +1,5 @@ // Modify and uncomment the following lines to configure login services. // Also see accounts/server/secrets.js -// Meteor.accounts.facebook.config('218833638237574', 'http://auth-todos.meteor.com'); -// Meteor.accounts.google.config('987846107089.apps.googleusercontent.com', 'http://auth-todos.meteor.com'); +// Accounts.facebook.config('218833638237574', 'http://auth-todos.meteor.com'); +// Accounts.google.config('987846107089.apps.googleusercontent.com', 'http://auth-todos.meteor.com'); diff --git a/packages/accounts-base/accounts_client.js b/packages/accounts-base/accounts_client.js index 4d8c6c02d2..c894b10f50 100644 --- a/packages/accounts-base/accounts_client.js +++ b/packages/accounts-base/accounts_client.js @@ -26,7 +26,7 @@ if (error) { callback && callback(error); } else { - Meteor.accounts.makeClientLoggedOut(); + Accounts.makeClientLoggedOut(); callback && callback(); } }); @@ -55,7 +55,7 @@ // loginServiceConfiguration subscription is ready. Used by // accounts-ui to hide the login button until we have all the // configuration loaded - Meteor.accounts.loginServicesConfigured = function () { + Accounts.loginServicesConfigured = function () { if (loginServicesConfigured) return true; diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index cee3fb010b..d3ae490a5a 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -1,9 +1,9 @@ -if (!Meteor.accounts) { - Meteor.accounts = {}; +if (!Accounts) { + Accounts = {}; } -if (!Meteor.accounts._options) { - Meteor.accounts._options = {}; +if (!Accounts._options) { + Accounts._options = {}; } // @param options {Object} an object with fields: @@ -11,13 +11,13 @@ if (!Meteor.accounts._options) { // - requireUsername {Boolean} // - validateEmails {Boolean} Send validation emails to all new users // via the signup form -Meteor.accounts.config = function(options) { - Meteor.accounts._options = options; +Accounts.config = function(options) { + Accounts._options = options; }; // internal login tokens collection. Never published. -Meteor.accounts._loginTokens = new Meteor.Collection( +Accounts._loginTokens = new Meteor.Collection( "accounts._loginTokens", null /*manager*/, null /*driver*/, @@ -27,7 +27,7 @@ Meteor.accounts._loginTokens = new Meteor.Collection( // table, and it is _really_ insecure to allow it as users could easily // steal sessions and impersonate other users. Users can override by // calling more allows later, if they really want. -Meteor.accounts._loginTokens.allow({}); +Accounts._loginTokens.allow({}); // Users table. Don't use the normal autopublish, since we want to hide @@ -43,7 +43,7 @@ Meteor.users = new Meteor.Collection( // Table containing documents with configuration options for each // login service -Meteor.accounts.configuration = new Meteor.Collection( +Accounts.configuration = new Meteor.Collection( "accounts._loginServiceConfiguration", null /*manager*/, null /*driver*/, @@ -56,18 +56,18 @@ Meteor.accounts.configuration = new Meteor.Collection( // Thrown when trying to use a login service which is not configured -Meteor.accounts.ConfigError = function(description) { +Accounts.ConfigError = function(description) { this.message = description; }; -Meteor.accounts.ConfigError.prototype = new Error(); -Meteor.accounts.ConfigError.prototype.name = 'Meteor.accounts.ConfigError'; +Accounts.ConfigError.prototype = new Error(); +Accounts.ConfigError.prototype.name = 'Accounts.ConfigError'; // Thrown when the user cancels the login process (eg, closes an oauth // popup, declines retina scan, etc) -Meteor.accounts.LoginCancelledError = function(description) { +Accounts.LoginCancelledError = function(description) { this.message = description; this.cancelled = true; }; -Meteor.accounts.LoginCancelledError.prototype = new Error(); -Meteor.accounts.LoginCancelledError.prototype.name = 'Meteor.accounts.LoginCancelledError'; +Accounts.LoginCancelledError.prototype = new Error(); +Accounts.LoginCancelledError.prototype.name = 'Accounts.LoginCancelledError'; diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 90d152623f..d687ff58a3 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -20,7 +20,7 @@ } }); - Meteor.accounts._loginHandlers = []; + Accounts._loginHandlers = []; // Try all of the registered login handlers until one of them // doesn't return `undefined` (NOT null), meaning it handled this @@ -28,7 +28,7 @@ var tryAllLoginHandlers = function (options) { var result = undefined; - _.find(Meteor.accounts._loginHandlers, function(handler) { + _.find(Accounts._loginHandlers, function(handler) { var maybeResult = handler(options); if (maybeResult !== undefined) { @@ -51,14 +51,14 @@ // - `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); + Accounts.registerLoginHandler = function(handler) { + Accounts._loginHandlers.push(handler); }; // support reconnecting using a meteor login token - Meteor.accounts.registerLoginHandler(function(options) { + Accounts.registerLoginHandler(function(options) { if (options.resume) { - var loginToken = Meteor.accounts._loginTokens + var loginToken = Accounts._loginTokens .findOne({_id: options.resume}); if (!loginToken) throw new Meteor.Error(403, "Couldn't find login token"); @@ -103,7 +103,7 @@ /// CREATE USER HOOKS /// var onCreateUserHook = null; - Meteor.accounts.onCreateUser = function (func) { + Accounts.onCreateUser = function (func) { if (onCreateUserHook) throw new Error("Can only call onCreateUser once"); else @@ -117,18 +117,18 @@ ['services', 'username', 'email', 'emails']))) throw new Meteor.Error(400, "Disallowed fields in extra"); - if (Meteor.accounts._options.requireEmail && + if (Accounts._options.requireEmail && (!user.emails || !user.emails.length)) throw new Meteor.Error(400, "Email address required."); - if (Meteor.accounts._options.requireUsername && + if (Accounts._options.requireUsername && !user.username) throw new Meteor.Error(400, "Username required."); return _.extend(user, extra); }; - Meteor.accounts.onCreateUserHook = function (options, extra, user) { + Accounts.onCreateUserHook = function (options, extra, user) { // add created at timestamp (and protect passed in user object from // modification) user = _.extend({createdAt: +(new Date)}, user); @@ -169,7 +169,7 @@ }; var validateNewUserHooks = []; - Meteor.accounts.validateNewUser = function (func) { + Accounts.validateNewUser = function (func) { validateNewUserHooks.push(func); }; @@ -184,7 +184,7 @@ // - services {Object} e.g. {facebook: {id: (facebook user id), ...}} // @param extra {Object, optional} Any additional fields to place on the user objet // @returns {String} userId - Meteor.accounts.updateOrCreateUser = function(options, extra) { + Accounts.updateOrCreateUser = function(options, extra) { extra = extra || {}; if (_.keys(options.services).length !== 1) @@ -212,7 +212,7 @@ user = { services: attrs }; - user = Meteor.accounts.onCreateUserHook(options, extra, user); + user = Accounts.onCreateUserHook(options, extra, user); return Meteor.users.insert(user); } }; @@ -242,15 +242,15 @@ // Publish all login service configuration fields other than secret. Meteor.publish("loginServiceConfiguration", function () { - return Meteor.accounts.configuration.find({}, {fields: {secret: 0}}); + return Accounts.configuration.find({}, {fields: {secret: 0}}); }, {is_auto: true}); // not techincally autopublish, but stops the warning. // Allow a one-time configuration for a login service. - Meteor.accounts.configuration.allow({}); // disallow mutators + Accounts.configuration.allow({}); // disallow mutators Meteor.methods({ "configureLoginService": function(options) { - if (!Meteor.accounts.configuration.findOne({service: options.service})) - Meteor.accounts.configuration.insert(options); + if (!Accounts.configuration.findOne({service: options.service})) + Accounts.configuration.insert(options); else throw new Meteor.Error(403, "Service " + options.service + " already configured"); } diff --git a/packages/accounts-base/accounts_tests.js b/packages/accounts-base/accounts_tests.js index 7147e9dbd9..e5086897b3 100644 --- a/packages/accounts-base/accounts_tests.js +++ b/packages/accounts-base/accounts_tests.js @@ -5,13 +5,13 @@ Tinytest.add('accounts - updateOrCreateUser', function (test) { // create an account with facebook - var uid1 = Meteor.accounts.updateOrCreateUser( + var uid1 = Accounts.updateOrCreateUser( {services: {facebook: {id: facebookId}}}, {foo: 1}); test.equal(Meteor.users.find({"services.facebook.id": facebookId}).count(), 1); test.equal(Meteor.users.findOne({"services.facebook.id": facebookId}).foo, 1); // create again with the same id, see that we get the same user - var uid2 = Meteor.accounts.updateOrCreateUser( + var uid2 = Accounts.updateOrCreateUser( {services: {facebook: {id: facebookId}}}, {foo: 1000, bar: 2}); // foo: 1000 shouldn't overwrite test.equal(uid1, uid2); test.equal(Meteor.users.find({"services.facebook.id": facebookId}).count(), 1); @@ -23,9 +23,9 @@ Tinytest.add('accounts - updateOrCreateUser', function (test) { // users that have different service ids get different users - uid1 = Meteor.accounts.updateOrCreateUser( + uid1 = Accounts.updateOrCreateUser( {services: {weibo: {id: weiboId1}}}, {foo: 1}); - uid2 = Meteor.accounts.updateOrCreateUser( + uid2 = Accounts.updateOrCreateUser( {services: {weibo: {id: weiboId2}}}, {bar: 2}); test.equal(Meteor.users.find({"services.weibo.id": {$in: [weiboId1, weiboId2]}}).count(), 2); test.equal(Meteor.users.findOne({"services.weibo.id": weiboId1}).foo, 1); @@ -45,7 +45,7 @@ Tinytest.add('accounts - onCreateUserHook username', function (test) { }; // user does not already exist. return a user object with fields set. - var userOut = Meteor.accounts.onCreateUserHook( + var userOut = Accounts.onCreateUserHook( userIn, {profile: {name: 'Foo Bar'}}, userIn @@ -60,7 +60,7 @@ Tinytest.add('accounts - onCreateUserHook username', function (test) { // run the hook again. now the user exists, so it throws an error. test.throws(function () { - Meteor.accounts.onCreateUserHook( + Accounts.onCreateUserHook( userIn, {profile: {name: 'Foo Bar'}}, userIn @@ -82,7 +82,7 @@ Tinytest.add('accounts - onCreateUserHook email', function (test) { }; // user does not already exist. return a user object with fields set. - var userOut = Meteor.accounts.onCreateUserHook( + var userOut = Accounts.onCreateUserHook( userIn, {profile: {name: 'Foo Bar'}}, userIn @@ -98,7 +98,7 @@ Tinytest.add('accounts - onCreateUserHook email', function (test) { // run the hook again with the exact same emails. // run the hook again. now the user exists, so it throws an error. test.throws(function () { - Meteor.accounts.onCreateUserHook( + Accounts.onCreateUserHook( userIn, {profile: {name: 'Foo Bar'}}, userIn @@ -107,20 +107,20 @@ Tinytest.add('accounts - onCreateUserHook email', function (test) { // now with only one of them. test.throws(function () { - Meteor.accounts.onCreateUserHook( + Accounts.onCreateUserHook( {}, {}, {emails: [{address: email1}]} ); }); test.throws(function () { - Meteor.accounts.onCreateUserHook( + Accounts.onCreateUserHook( {}, {}, {emails: [{address: email2}]} ); }); // a third email works. - var user3 = Meteor.accounts.onCreateUserHook( + var user3 = Accounts.onCreateUserHook( {}, {}, {emails: [{address: email3}]} ); test.equal(typeof userOut.createdAt, 'number'); diff --git a/packages/accounts-base/localstorage_token.js b/packages/accounts-base/localstorage_token.js index 9d14a7fcfa..43391a88de 100644 --- a/packages/accounts-base/localstorage_token.js +++ b/packages/accounts-base/localstorage_token.js @@ -3,45 +3,45 @@ var loginTokenKey = "Meteor.loginToken"; var userIdKey = "Meteor.userId"; - Meteor.accounts.storeLoginToken = function(userId, token) { + Accounts.storeLoginToken = function(userId, token) { localStorage.setItem(userIdKey, userId); localStorage.setItem(loginTokenKey, token); // to ensure that the localstorage poller doesn't end up trying to // connect a second time - Meteor.accounts._lastLoginTokenWhenPolled = token; + Accounts._lastLoginTokenWhenPolled = token; }; - Meteor.accounts.unstoreLoginToken = function() { + Accounts.unstoreLoginToken = function() { localStorage.removeItem(userIdKey); localStorage.removeItem(loginTokenKey); // to ensure that the localstorage poller doesn't end up trying to // connect a second time - Meteor.accounts._lastLoginTokenWhenPolled = null; + Accounts._lastLoginTokenWhenPolled = null; }; - Meteor.accounts.storedLoginToken = function() { + Accounts.storedLoginToken = function() { return localStorage.getItem(loginTokenKey); }; - Meteor.accounts.storedUserId = function() { + Accounts.storedUserId = function() { return localStorage.getItem(userIdKey); }; - Meteor.accounts.makeClientLoggedOut = function() { - Meteor.accounts.unstoreLoginToken(); + Accounts.makeClientLoggedOut = function() { + Accounts.unstoreLoginToken(); Meteor.default_connection.setUserId(null); Meteor.default_connection.onReconnect = null; }; - Meteor.accounts.makeClientLoggedIn = function(userId, token) { - Meteor.accounts.storeLoginToken(userId, token); + Accounts.makeClientLoggedIn = function(userId, token) { + Accounts.storeLoginToken(userId, token); Meteor.default_connection.setUserId(userId); Meteor.default_connection.onReconnect = function() { Meteor.apply('login', [{resume: token}], {wait: true}, function(error, result) { if (error) { - Meteor.accounts.makeClientLoggedOut(); + Accounts.makeClientLoggedOut(); throw error; } else { // nothing to do @@ -62,49 +62,49 @@ Meteor.loginWithToken = function (token, errorCallback) { throw error; } - Meteor.accounts.makeClientLoggedIn(result.id, result.token); + Accounts.makeClientLoggedIn(result.id, result.token); }); }; -if (!Meteor.accounts._preventAutoLogin) { +if (!Accounts._preventAutoLogin) { // Immediately try to log in via local storage, so that any DDP // messages are sent after we have established our user account - var token = Meteor.accounts.storedLoginToken(); + var token = Accounts.storedLoginToken(); if (token) { // On startup, optimistically present us as logged in while the // request is in flight. This reduces page flicker on startup. - var userId = Meteor.accounts.storedUserId(); + var userId = Accounts.storedUserId(); userId && Meteor.default_connection.setUserId(userId); Meteor.loginWithToken(token, function () { - Meteor.accounts.makeClientLoggedOut(); + Accounts.makeClientLoggedOut(); }); } } // Poll local storage every 3 seconds to login if someone logged in in // another tab -Meteor.accounts._lastLoginTokenWhenPolled = token; -Meteor.accounts._pollStoredLoginToken = function() { - if (Meteor.accounts._preventAutoLogin) +Accounts._lastLoginTokenWhenPolled = token; +Accounts._pollStoredLoginToken = function() { + if (Accounts._preventAutoLogin) return; - var currentLoginToken = Meteor.accounts.storedLoginToken(); + var currentLoginToken = Accounts.storedLoginToken(); // != instead of !== just to make sure undefined and null are treated the same - if (Meteor.accounts._lastLoginTokenWhenPolled != currentLoginToken) { + if (Accounts._lastLoginTokenWhenPolled != currentLoginToken) { if (currentLoginToken) Meteor.loginWithToken(currentLoginToken); // XXX should we pass a callback here? else Meteor.logout(); } - Meteor.accounts._lastLoginTokenWhenPolled = currentLoginToken; + Accounts._lastLoginTokenWhenPolled = currentLoginToken; }; // Semi-internal API. Call this function to re-enable auto login after // if it was disabled at startup. -Meteor.accounts._enableAutoLogin = function () { - Meteor.accounts._preventAutoLogin = false; - Meteor.accounts._pollStoredLoginToken(); +Accounts._enableAutoLogin = function () { + Accounts._preventAutoLogin = false; + Accounts._pollStoredLoginToken(); }; -setInterval(Meteor.accounts._pollStoredLoginToken, 3000); +setInterval(Accounts._pollStoredLoginToken, 3000); diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js index e92e3cbd1a..a3f348cb4f 100644 --- a/packages/accounts-facebook/facebook_client.js +++ b/packages/accounts-facebook/facebook_client.js @@ -1,8 +1,8 @@ (function () { Meteor.loginWithFacebook = function (callback) { - var config = Meteor.accounts.configuration.findOne({service: 'facebook'}); + var config = Accounts.configuration.findOne({service: 'facebook'}); if (!config) { - callback && callback(new Meteor.accounts.ConfigError("Service not configured")); + callback && callback(new Accounts.ConfigError("Service not configured")); return; } @@ -11,16 +11,16 @@ var display = mobile ? 'touch' : 'popup'; var scope = "email"; - if (Meteor.accounts.facebook._options && - Meteor.accounts.facebook._options.scope) - scope = Meteor.accounts.facebook._options.scope.join(','); + if (Accounts.facebook._options && + Accounts.facebook._options.scope) + scope = Accounts.facebook._options.scope.join(','); var loginUrl = '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, callback); + Accounts.oauth.initiateLogin(state, loginUrl, callback); }; })(); diff --git a/packages/accounts-facebook/facebook_common.js b/packages/accounts-facebook/facebook_common.js index 2c1fca99f9..d04176578e 100644 --- a/packages/accounts-facebook/facebook_common.js +++ b/packages/accounts-facebook/facebook_common.js @@ -1,7 +1,7 @@ -if (!Meteor.accounts.facebook) { - Meteor.accounts.facebook = {}; +if (!Accounts.facebook) { + Accounts.facebook = {}; } -Meteor.accounts.facebook.config = function(options) { - Meteor.accounts.facebook._options = options; +Accounts.facebook.config = function(options) { + Accounts.facebook._options = options; }; diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index 0ce7db31c4..07bf56c52b 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -1,6 +1,6 @@ (function () { - Meteor.accounts.oauth.registerService('facebook', 2, function(query) { + Accounts.oauth.registerService('facebook', 2, function(query) { var accessToken = getAccessToken(query); var identity = getIdentity(accessToken); @@ -18,9 +18,9 @@ }); var getAccessToken = function (query) { - var config = Meteor.accounts.configuration.findOne({service: 'facebook'}); + var config = Accounts.configuration.findOne({service: 'facebook'}); if (!config) - throw new Meteor.accounts.ConfigError("Service not configured"); + throw new Accounts.ConfigError("Service not configured"); // Request an access token var result = Meteor.http.get( diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index 81cc861de9..697a9c6a4f 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -1,8 +1,8 @@ (function () { Meteor.loginWithGoogle = function (callback) { - var config = Meteor.accounts.configuration.findOne({service: 'google'}); + var config = Accounts.configuration.findOne({service: 'google'}); if (!config) { - callback && callback(new Meteor.accounts.ConfigError("Service not configured")); + callback && callback(new Accounts.ConfigError("Service not configured")); return; } @@ -11,9 +11,9 @@ // always need this to get user id from google. var required_scope = ['https://www.googleapis.com/auth/userinfo.profile']; var scope = ['https://www.googleapis.com/auth/userinfo.email']; - if (Meteor.accounts.google._options && - Meteor.accounts.google._options.scope) - scope = Meteor.accounts.google._options.scope; + if (Accounts.google._options && + Accounts.google._options.scope) + scope = Accounts.google._options.scope; scope = _.union(scope, required_scope); var flat_scope = _.map(scope, encodeURIComponent).join('+'); @@ -28,7 +28,7 @@ '&redirect_uri=' + Meteor.absoluteUrl('_oauth/google?close') + '&state=' + state; - Meteor.accounts.oauth.initiateLogin(state, loginUrl, callback); + Accounts.oauth.initiateLogin(state, loginUrl, callback); }; }) (); diff --git a/packages/accounts-google/google_common.js b/packages/accounts-google/google_common.js index b68429d14b..8ca64f59f9 100644 --- a/packages/accounts-google/google_common.js +++ b/packages/accounts-google/google_common.js @@ -1,7 +1,7 @@ -if (!Meteor.accounts.google) { - Meteor.accounts.google = {}; +if (!Accounts.google) { + Accounts.google = {}; } -Meteor.accounts.google.config = function(options) { - Meteor.accounts.google._options = options; +Accounts.google.config = function(options) { + Accounts.google._options = options; }; diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 85903a10be..8da55b0fb4 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -1,10 +1,10 @@ (function () { - Meteor.accounts.google.setSecret = function (secret) { - Meteor.accounts.google._secret = secret; + Accounts.google.setSecret = function (secret) { + Accounts.google._secret = secret; }; - Meteor.accounts.oauth.registerService('google', 2, function(query) { + Accounts.oauth.registerService('google', 2, function(query) { var accessToken = getAccessToken(query); var identity = getIdentity(accessToken); @@ -22,9 +22,9 @@ }); var getAccessToken = function (query) { - var config = Meteor.accounts.configuration.findOne({service: 'google'}); + var config = Accounts.configuration.findOne({service: 'google'}); if (!config) - throw new Meteor.accounts.ConfigError("Service not configured"); + throw new Accounts.ConfigError("Service not configured"); var result = Meteor.http.post( "https://accounts.google.com/o/oauth2/token", {params: { diff --git a/packages/accounts-oauth-helper/oauth_client.js b/packages/accounts-oauth-helper/oauth_client.js index 763390df01..16dfdacbff 100644 --- a/packages/accounts-oauth-helper/oauth_client.js +++ b/packages/accounts-oauth-helper/oauth_client.js @@ -6,7 +6,7 @@ // @param callback {Function} Callback function to call on // completion. Takes one argument, null on success, or Error on // error. - Meteor.accounts.oauth.initiateLogin = function(state, url, callback) { + Accounts.oauth.initiateLogin = function(state, url, callback) { // XXX these dimensions worked well for facebook and google, but // it's sort of weird to have these here. Maybe an optional // argument instead? @@ -45,9 +45,9 @@ // the server doesn't see the request but does close the // window. This seems unlikely. callback && - callback(new Meteor.accounts.LoginCancelledError("Popup closed")); + callback(new Accounts.LoginCancelledError("Popup closed")); } else { - Meteor.accounts.makeClientLoggedIn(result.id, result.token); + Accounts.makeClientLoggedIn(result.id, result.token); callback && callback(); } }); diff --git a/packages/accounts-oauth-helper/oauth_common.js b/packages/accounts-oauth-helper/oauth_common.js index 3c6e8b8558..d47da20292 100644 --- a/packages/accounts-oauth-helper/oauth_common.js +++ b/packages/accounts-oauth-helper/oauth_common.js @@ -1 +1 @@ -Meteor.accounts.oauth = {}; \ No newline at end of file +Accounts.oauth = {}; \ No newline at end of file diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index 15f8e2e6a5..2fc7e6eef5 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -1,7 +1,7 @@ (function () { var connect = __meteor_bootstrap__.require("connect"); - Meteor.accounts.oauth._services = {}; + Accounts.oauth._services = {}; // Register a handler for an OAuth service. The handler will be called // when we get an incoming http request on /_oauth/{serviceName}. This @@ -15,13 +15,13 @@ // - (For OAuth2 only) query {Object} parameters passed in query string // - return value is: // - {options: (options), extra: (optional extra)} (same as the - // arguments to Meteor.accounts.updateOrCreateUser) + // arguments to Accounts.updateOrCreateUser) // - `null` if the user declined to give permissions - Meteor.accounts.oauth.registerService = function (name, version, handleOauthRequest) { - if (Meteor.accounts.oauth._services[name]) + Accounts.oauth.registerService = function (name, version, handleOauthRequest) { + if (Accounts.oauth._services[name]) throw new Error("Already registered the " + name + " OAuth service"); - Meteor.accounts.oauth._services[name] = { + Accounts.oauth._services[name] = { serviceName: name, version: version, handleOauthRequest: handleOauthRequest @@ -34,14 +34,14 @@ // method is called. Maps state --> return value of `login` // // XXX we should periodically clear old entries - Meteor.accounts.oauth._loginResultForState = {}; + Accounts.oauth._loginResultForState = {}; // Listen to calls to `login` with an oauth option set - Meteor.accounts.registerLoginHandler(function (options) { + Accounts.registerLoginHandler(function (options) { if (!options.oauth) return undefined; // don't handle - var result = Meteor.accounts.oauth._loginResultForState[options.oauth.state]; + var result = Accounts.oauth._loginResultForState[options.oauth.state]; if (result === undefined) // not using `!result` since can be null // We weren't notified of the user authorizing the login. return null; @@ -61,11 +61,11 @@ // calls and nothing else is wrapping this in a fiber // automatically Fiber(function () { - Meteor.accounts.oauth._middleware(req, res, next); + Accounts.oauth._middleware(req, res, next); }).run(); }); - Meteor.accounts.oauth._middleware = function (req, res, next) { + Accounts.oauth._middleware = function (req, res, next) { // Make sure to catch any exceptions because otherwise we'd crash // the runner try { @@ -76,7 +76,7 @@ return; } - var service = Meteor.accounts.oauth._services[serviceName]; + var service = Accounts.oauth._services[serviceName]; // Skip everything if there's no service set by the oauth middleware if (!service) @@ -86,9 +86,9 @@ ensureConfigured(serviceName); if (service.version === 1) - Meteor.accounts.oauth1._handleRequest(service, req.query, res); + Accounts.oauth1._handleRequest(service, req.query, res); else if (service.version === 2) - Meteor.accounts.oauth2._handleRequest(service, req.query, res); + Accounts.oauth2._handleRequest(service, req.query, res); else throw new Error("Unexpected OAuth version " + service.version); } catch (err) { @@ -100,7 +100,7 @@ // we were passed. But then the developer wouldn't be able to // style the error or react to it in any way. if (req.query.state && err instanceof Error) - Meteor.accounts.oauth._loginResultForState[req.query.state] = err; + Accounts.oauth._loginResultForState[req.query.state] = err; // also log to the server console, so the developer sees it. Meteor._debug("Exception in oauth request handler", err); @@ -108,7 +108,7 @@ // XXX the following is actually wrong. if someone wants to // redirect rather than close once we are done with the OAuth // flow, as supported by - // Meteor.accounts.oauth_renderOauthResults, this will still + // Accounts.oauth_renderOauthResults, this will still // close the popup instead. Once we fully support the redirect // flow (by supporting that in places such as // packages/facebook/facebook_client.js) we should revisit this. @@ -142,12 +142,12 @@ // Make sure we're configured var ensureConfigured = function(serviceName) { - if (!Meteor.accounts.configuration.findOne({service: serviceName})) { - throw new Meteor.accounts.ConfigError("Service not configured"); + if (!Accounts.configuration.findOne({service: serviceName})) { + throw new Accounts.ConfigError("Service not configured"); }; }; - Meteor.accounts.oauth._renderOauthResults = function(res, query) { + Accounts.oauth._renderOauthResults = function(res, query) { // We support ?close and ?redirect=URL. Any other query should // just serve a blank page if ('close' in query) { // check with 'in' because we don't set a value diff --git a/packages/accounts-oauth1-helper/oauth1_common.js b/packages/accounts-oauth1-helper/oauth1_common.js index 3b746c7e43..d4ce446298 100644 --- a/packages/accounts-oauth1-helper/oauth1_common.js +++ b/packages/accounts-oauth1-helper/oauth1_common.js @@ -1 +1 @@ -Meteor.accounts.oauth1 = {}; \ No newline at end of file +Accounts.oauth1 = {}; \ No newline at end of file diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 4025862d4d..b6a20bf30e 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -2,17 +2,17 @@ var connect = __meteor_bootstrap__.require("connect"); // A place to store request tokens pending verification - Meteor.accounts.oauth1._requestTokens = {}; + Accounts.oauth1._requestTokens = {}; // connect middleware - Meteor.accounts.oauth1._handleRequest = function (service, query, res) { + Accounts.oauth1._handleRequest = function (service, query, res) { - var config = Meteor.accounts.configuration.findOne({service: service.serviceName}); + var config = Accounts.configuration.findOne({service: service.serviceName}); if (!config) { - throw new Meteor.accounts.ConfigError("Service " + service.serviceName + " not configured"); + throw new Accounts.ConfigError("Service " + service.serviceName + " not configured"); } - var urls = Meteor.accounts[service.serviceName]._urls; + var urls = Accounts[service.serviceName]._urls; var oauthBinding = new OAuth1Binding( config.consumerKey, config.secret, urls); @@ -23,7 +23,7 @@ oauthBinding.prepareRequestToken(query.requestTokenAndRedirect); // Keep track of request token so we can verify it on the next step - Meteor.accounts.oauth1._requestTokens[query.state] = oauthBinding.requestToken; + Accounts.oauth1._requestTokens[query.state] = oauthBinding.requestToken; // redirect to provider login, which will redirect back to "step 2" below var redirectUrl = urls.authenticate + '?oauth_token=' + oauthBinding.requestToken; @@ -36,8 +36,8 @@ // token and access token secret and log in as user // Get the user's request token so we can verify it and clear it - var requestToken = Meteor.accounts.oauth1._requestTokens[query.state]; - delete Meteor.accounts.oauth1._requestTokens[query.state]; + var requestToken = Accounts.oauth1._requestTokens[query.state]; + delete Accounts.oauth1._requestTokens[query.state]; // Verify user authorized access and the oauth_token matches // the requestToken from previous step @@ -51,22 +51,22 @@ // Get or create user id var oauthResult = service.handleOauthRequest(oauthBinding); - var userId = Meteor.accounts.updateOrCreateUser( + var userId = Accounts.updateOrCreateUser( oauthResult.options, oauthResult.extra); // Generate and store a login token for reconnect // XXX this could go in accounts_server.js instead - var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); + var loginToken = Accounts._loginTokens.insert({userId: userId}); // Store results to subsequent call to `login` - Meteor.accounts.oauth._loginResultForState[query.state] = + Accounts.oauth._loginResultForState[query.state] = {token: loginToken, id: userId}; } } // Either close the window, redirect, or render nothing // if all else fails - Meteor.accounts.oauth._renderOauthResults(res, query); + Accounts.oauth._renderOauthResults(res, query); }; })(); diff --git a/packages/accounts-oauth1-helper/oauth1_tests.js b/packages/accounts-oauth1-helper/oauth1_tests.js index e081ed7c1c..856e40c625 100644 --- a/packages/accounts-oauth1-helper/oauth1_tests.js +++ b/packages/accounts-oauth1-helper/oauth1_tests.js @@ -14,16 +14,16 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) { // XXX XXX test isolation fail! Avital: but actually -- why would // we run server tests more than once? or even more so in parallel? - Meteor.accounts._loginTokens.remove({}); - Meteor.accounts.oauth._loginResultForState = {}; - Meteor.accounts.oauth._services = {}; + Accounts._loginTokens.remove({}); + Accounts.oauth._loginResultForState = {}; + Accounts.oauth._services = {}; - if (!Meteor.accounts.configuration.findOne({service: 'twitterfoo'})) - Meteor.accounts.configuration.insert({service: 'twitterfoo'}); - Meteor.accounts.twitterfoo = {}; + if (!Accounts.configuration.findOne({service: 'twitterfoo'})) + Accounts.configuration.insert({service: 'twitterfoo'}); + Accounts.twitterfoo = {}; // register a fake login service - twitterfoo - Meteor.accounts.oauth.registerService("twitterfoo", 1, function (query) { + Accounts.oauth.registerService("twitterfoo", 1, function (query) { return { options: { services: { @@ -39,7 +39,7 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) { }); // simulate logging in using twitterfoo - Meteor.accounts.oauth1._requestTokens['STATE'] = twitterfooAccessToken; + Accounts.oauth1._requestTokens['STATE'] = twitterfooAccessToken; var req = { method: "POST", @@ -50,7 +50,7 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) { } }; - Meteor.accounts.oauth._middleware(req, new http.ServerResponse(req)); + Accounts.oauth._middleware(req, new http.ServerResponse(req)); // verify that a user is created var user = Meteor.users.findOne({"services.twitter.screenName": twitterfooName}); @@ -59,14 +59,14 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) { test.equal(user.services.twitter.accessTokenSecret, twitterfooAccessTokenSecret); // and that that user has a login token - var token = Meteor.accounts._loginTokens.findOne({userId: user._id}); + var token = Accounts._loginTokens.findOne({userId: user._id}); test.notEqual(token, undefined); // and that the login result for that user is prepared test.equal( - Meteor.accounts.oauth._loginResultForState['STATE'].id, user._id); + Accounts.oauth._loginResultForState['STATE'].id, user._id); test.equal( - Meteor.accounts.oauth._loginResultForState['STATE'].token, token._id); + Accounts.oauth._loginResultForState['STATE'].token, token._id); }); @@ -78,15 +78,15 @@ 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 = {}; + if (!Accounts.configuration.findOne({service: 'twitterfail'})) + Accounts.configuration.insert({service: 'twitterfail'}); + Accounts.twitterfail = {}; // Wire up access token so that verification passes - Meteor.accounts.oauth1._requestTokens[state] = twitterfailAccessToken; + Accounts.oauth1._requestTokens[state] = twitterfailAccessToken; // register a failing login service - Meteor.accounts.oauth.registerService("twitterfail", 1, function (query) { + Accounts.oauth.registerService("twitterfail", 1, function (query) { return { options: { services: { @@ -106,7 +106,7 @@ Tinytest.add("oauth1 - error in user creation", function (test) { // a way to fail new users. duplicated from passwords_tests, but // shouldn't hurt. - Meteor.accounts.validateNewUser(function (user) { + Accounts.validateNewUser(function (user) { return !user.invalid; }); @@ -121,14 +121,14 @@ Tinytest.add("oauth1 - error in user creation", function (test) { } }; - Meteor.accounts.oauth._middleware(req, new http.ServerResponse(req)); + Accounts.oauth._middleware(req, new http.ServerResponse(req)); // verify that a user is not created var user = Meteor.users.findOne({"services.twitter.screenName": twitterfailName}); test.equal(user, undefined); // verify an error is stored in login state - test.equal(Meteor.accounts.oauth._loginResultForState[state].error, 403); + test.equal(Accounts.oauth._loginResultForState[state].error, 403); // verify error is handed back to login method. test.throws(function () { diff --git a/packages/accounts-oauth2-helper/oauth2_common.js b/packages/accounts-oauth2-helper/oauth2_common.js index cb23a48c2d..0012a34cee 100644 --- a/packages/accounts-oauth2-helper/oauth2_common.js +++ b/packages/accounts-oauth2-helper/oauth2_common.js @@ -1 +1 @@ -Meteor.accounts.oauth2 = {}; \ No newline at end of file +Accounts.oauth2 = {}; \ No newline at end of file diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index 718d7fd4b6..3e75759947 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -2,7 +2,7 @@ var connect = __meteor_bootstrap__.require("connect"); // connect middleware - Meteor.accounts.oauth2._handleRequest = function (service, query, res) { + Accounts.oauth2._handleRequest = function (service, query, res) { // check if user authorized access if (!query.error) { // Prepare the login results before returning. This way the @@ -11,21 +11,21 @@ // Get or create user id var oauthResult = service.handleOauthRequest(query); - var userId = Meteor.accounts.updateOrCreateUser( + var userId = Accounts.updateOrCreateUser( oauthResult.options, oauthResult.extra); // Generate and store a login token for reconnect // XXX this could go in accounts_server.js instead - var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); + var loginToken = Accounts._loginTokens.insert({userId: userId}); // Store results to subsequent call to `login` - Meteor.accounts.oauth._loginResultForState[query.state] = + Accounts.oauth._loginResultForState[query.state] = {token: loginToken, id: userId}; } // Either close the window, redirect, or render nothing // if all else fails - Meteor.accounts.oauth._renderOauthResults(res, query); + Accounts.oauth._renderOauthResults(res, query); }; })(); diff --git a/packages/accounts-oauth2-helper/oauth2_tests.js b/packages/accounts-oauth2-helper/oauth2_tests.js index 56171fec8b..d1da4d311e 100644 --- a/packages/accounts-oauth2-helper/oauth2_tests.js +++ b/packages/accounts-oauth2-helper/oauth2_tests.js @@ -4,16 +4,16 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { // XXX XXX test isolation fail! Avital: but actually -- why would // we run server tests more than once? or even more so in parallel? - Meteor.accounts._loginTokens.remove({}); - Meteor.accounts.oauth._loginResultForState = {}; - Meteor.accounts.oauth._services = {}; + Accounts._loginTokens.remove({}); + Accounts.oauth._loginResultForState = {}; + Accounts.oauth._services = {}; - if (!Meteor.accounts.configuration.findOne({service: 'foobook'})) - Meteor.accounts.configuration.insert({service: 'foobook'}); - Meteor.accounts.foobook = {}; + if (!Accounts.configuration.findOne({service: 'foobook'})) + Accounts.configuration.insert({service: 'foobook'}); + Accounts.foobook = {}; // register a fake login service - foobook - Meteor.accounts.oauth.registerService("foobook", 2, function (query) { + Accounts.oauth.registerService("foobook", 2, function (query) { return { options: { services: {foobook: {id: foobookId}} @@ -25,7 +25,7 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { var req = {method: "POST", url: "/_oauth/foobook?close", query: {state: "STATE"}}; - Meteor.accounts.oauth._middleware(req, new http.ServerResponse(req)); + Accounts.oauth._middleware(req, new http.ServerResponse(req)); // verify that a user is created var user = Meteor.users.findOne({"services.foobook.id": foobookId}); @@ -33,14 +33,14 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { test.equal(user.services.foobook.id, foobookId); // and that that user has a login token - var token = Meteor.accounts._loginTokens.findOne({userId: user._id}); + var token = Accounts._loginTokens.findOne({userId: user._id}); test.notEqual(token, undefined); // and that the login result for that user is prepared test.equal( - Meteor.accounts.oauth._loginResultForState['STATE'].id, user._id); + Accounts.oauth._loginResultForState['STATE'].id, user._id); test.equal( - Meteor.accounts.oauth._loginResultForState['STATE'].token, token._id); + Accounts.oauth._loginResultForState['STATE'].token, token._id); }); @@ -49,12 +49,12 @@ 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 = {}; + if (!Accounts.configuration.findOne({service: 'failbook'})) + Accounts.configuration.insert({service: 'failbook'}); + Accounts.failbook = {}; // register a failing login service - Meteor.accounts.oauth.registerService("failbook", 2, function (query) { + Accounts.oauth.registerService("failbook", 2, function (query) { return { options: { services: {failbook: {id: failbookId}} @@ -67,7 +67,7 @@ Tinytest.add("oauth2 - error in user creation", function (test) { // a way to fail new users. duplicated from passwords_tests, but // shouldn't hurt. - Meteor.accounts.validateNewUser(function (user) { + Accounts.validateNewUser(function (user) { return !user.invalid; }); @@ -76,14 +76,14 @@ Tinytest.add("oauth2 - error in user creation", function (test) { var req = {method: "POST", url: "/_oauth/failbook?close", query: {state: state}}; - Meteor.accounts.oauth._middleware(req, new http.ServerResponse(req)); + Accounts.oauth._middleware(req, new http.ServerResponse(req)); // verify that a user is not created var user = Meteor.users.findOne({"services.failbook.id": failbookId}); test.equal(user, undefined); // verify an error is stored in login state - test.equal(Meteor.accounts.oauth._loginResultForState[state].error, 403); + test.equal(Accounts.oauth._loginResultForState[state].error, 403); // verify error is handed back to login method. test.throws(function () { diff --git a/packages/accounts-password/email_templates.js b/packages/accounts-password/email_templates.js index d5fea9fbd2..d3275b4ed5 100644 --- a/packages/accounts-password/email_templates.js +++ b/packages/accounts-password/email_templates.js @@ -1,10 +1,10 @@ -Meteor.accounts.emailTemplates = { +Accounts.emailTemplates = { from: "Meteor Accounts ", siteName: Meteor.absoluteUrl().replace(/^https?:\/\//, '').replace(/\/$/, ''), resetPassword: { subject: function(user) { - return "How to reset your password on " + Meteor.accounts.emailTemplates.siteName; + return "How to reset your password on " + Accounts.emailTemplates.siteName; }, text: function(user, url) { var greeting = (user.profile && user.profile.name) ? @@ -20,7 +20,7 @@ Meteor.accounts.emailTemplates = { }, validateEmail: { subject: function(user) { - return "How to validate your account email on " + Meteor.accounts.emailTemplates.siteName; + return "How to validate your account email on " + Accounts.emailTemplates.siteName; }, text: function(user, url) { var greeting = (user.profile && user.profile.name) ? @@ -36,7 +36,7 @@ Meteor.accounts.emailTemplates = { }, enrollAccount: { subject: function(user) { - return "An account has been created for you on " + Meteor.accounts.emailTemplates.siteName; + return "An account has been created for you on " + Accounts.emailTemplates.siteName; }, text: function(user, url) { var greeting = (user.profile && user.profile.name) ? diff --git a/packages/accounts-password/email_tests_setup.js b/packages/accounts-password/email_tests_setup.js index e7afd89d7f..c932f62a53 100644 --- a/packages/accounts-password/email_tests_setup.js +++ b/packages/accounts-password/email_tests_setup.js @@ -28,7 +28,7 @@ Meteor.users.update( {_id: this.userId()}, {$push: {emails: {address: email, validated: false}}}); - Meteor.accounts.sendValidationEmail(this.userId(), email); + Accounts.sendValidationEmail(this.userId(), email); }, createUserOnServer: function (email) { diff --git a/packages/accounts-password/passwords_client.js b/packages/accounts-password/passwords_client.js index 83b066042f..ab4f7b09ab 100644 --- a/packages/accounts-password/passwords_client.js +++ b/packages/accounts-password/passwords_client.js @@ -22,7 +22,7 @@ return; } - Meteor.accounts.makeClientLoggedIn(result.id, result.token); + Accounts.makeClientLoggedIn(result.id, result.token); callback && callback(undefined, {message: 'Success'}); }); }; @@ -68,7 +68,7 @@ return; } - Meteor.accounts.makeClientLoggedIn(result.id, result.token); + Accounts.makeClientLoggedIn(result.id, result.token); callback && callback(); }); }); @@ -158,13 +158,13 @@ callback && callback(error); } - Meteor.accounts.makeClientLoggedIn(result.id, result.token); + Accounts.makeClientLoggedIn(result.id, result.token); callback && callback(); }); }; // Validates a user's email address based on a token originally - // created by Meteor.accounts.sendValidationEmail + // created by Accounts.sendValidationEmail // // @param token {String} // @param callback (optional) {Function(error|undefined)} diff --git a/packages/accounts-password/passwords_common.js b/packages/accounts-password/passwords_common.js index 1bf1351dae..69ae38975d 100644 --- a/packages/accounts-password/passwords_common.js +++ b/packages/accounts-password/passwords_common.js @@ -1 +1 @@ -Meteor.accounts.passwords = {}; +Accounts.passwords = {}; diff --git a/packages/accounts-password/passwords_server.js b/packages/accounts-password/passwords_server.js index 2cba4db4e1..e771835dde 100644 --- a/packages/accounts-password/passwords_server.js +++ b/packages/accounts-password/passwords_server.js @@ -1,7 +1,7 @@ (function () { // internal verifier collection. Never published. - Meteor.accounts._srpChallenges = new Meteor.Collection( + Accounts._srpChallenges = new Meteor.Collection( "accounts._srpChallenges", null /*manager*/, null /*driver*/, @@ -11,16 +11,16 @@ // table, and it is _really_ insecure to allow it as users could easily // steal sessions and impersonate other users. Users can override by // calling more allows later, if they really want. - Meteor.accounts._srpChallenges.allow({}); + Accounts._srpChallenges.allow({}); // internal email validation tokens collection. Never published. - Meteor.accounts._emailValidationTokens = new Meteor.Collection( + Accounts._emailValidationTokens = new Meteor.Collection( "accounts._emailValidationTokens", null /*manager*/, null /*driver*/, true /*preventAutopublish*/); // also lock down email validation. These can be used to log in. - Meteor.accounts._emailValidationTokens.allow({}); + Accounts._emailValidationTokens.allow({}); var selectorFromUserQuery = function (user) { @@ -77,7 +77,7 @@ // and then log in as you (but no more insecure than reconnect // tokens). var serialized = { userId: user._id, M: srp.M, HAMK: srp.HAMK }; - Meteor.accounts._srpChallenges.insert(serialized); + Accounts._srpChallenges.insert(serialized); return challenge; }, @@ -94,7 +94,7 @@ } if (options.M) { - var serialized = Meteor.accounts._srpChallenges.findOne( + var serialized = Accounts._srpChallenges.findOne( {M: options.M}); if (!serialized) throw new Meteor.Error(403, "Incorrect password"); @@ -138,12 +138,12 @@ } }}); - var resetPasswordUrl = Meteor.accounts.urls.resetPassword(token); + var resetPasswordUrl = Accounts.urls.resetPassword(token); Email.send({ to: email, - from: Meteor.accounts.emailTemplates.from, - subject: Meteor.accounts.emailTemplates.resetPassword.subject(user), - text: Meteor.accounts.emailTemplates.resetPassword.text(user, resetPasswordUrl)}); + from: Accounts.emailTemplates.from, + subject: Accounts.emailTemplates.resetPassword.subject(user), + text: Accounts.emailTemplates.resetPassword.text(user, resetPasswordUrl)}); }, resetPassword: function (token, newVerifier) { @@ -165,7 +165,7 @@ {$set: {"emails.0.validated": true}}); - var loginToken = Meteor.accounts._loginTokens.insert({userId: user._id}); + var loginToken = Accounts._loginTokens.insert({userId: user._id}); this.setUserId(user._id); return {token: loginToken, id: user._id}; }, @@ -174,7 +174,7 @@ if (!token) throw new Meteor.Error(400, "Need to pass token"); - var tokenDocument = Meteor.accounts._emailValidationTokens.findOne( + var tokenDocument = Accounts._emailValidationTokens.findOne( {token: token}); if (!tokenDocument) throw new Meteor.Error(403, "Validate email link expired"); @@ -186,9 +186,9 @@ // http://www.mongodb.org/display/DOCS/Updating/#Updating-The%24positionaloperator) Meteor.users.update({_id: userId, "emails.address": email}, {$set: {"emails.$.validated": true}}); - Meteor.accounts._emailValidationTokens.remove({token: token}); + Accounts._emailValidationTokens.remove({token: token}); - var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); + var loginToken = Accounts._loginTokens.insert({userId: userId}); this.setUserId(userId); return {token: loginToken, id: userId}; } @@ -196,10 +196,10 @@ // send the user an email with a link that when opened marks that // address as validated - Meteor.accounts.sendValidationEmail = function (userId, email) { + Accounts.sendValidationEmail = function (userId, email) { var token = Meteor.uuid(); var when = +(new Date); - Meteor.accounts._emailValidationTokens.insert({ + Accounts._emailValidationTokens.insert({ email: email, token: token, when: when, @@ -211,19 +211,19 @@ // this account. var user = Meteor.users.findOne(userId); - var validateEmailUrl = Meteor.accounts.urls.validateEmail(token); + var validateEmailUrl = Accounts.urls.validateEmail(token); Email.send({ to: email, - from: Meteor.accounts.emailTemplates.from, - subject: Meteor.accounts.emailTemplates.validateEmail.subject(user), - text: Meteor.accounts.emailTemplates.validateEmail.text(user, validateEmailUrl) + from: Accounts.emailTemplates.from, + subject: Accounts.emailTemplates.validateEmail.subject(user), + text: Accounts.emailTemplates.validateEmail.text(user, validateEmailUrl) }); }; // send the user an email informing them that their account was // created, with a link that when opened both marks their email as // validated and forces them to choose their password - Meteor.accounts.sendEnrollmentEmail = function (userId, email) { + Accounts.sendEnrollmentEmail = function (userId, email) { var token = Meteor.uuid(); var when = +(new Date); Meteor.users.update(userId, {$set: { @@ -234,29 +234,29 @@ }}); var user = Meteor.users.findOne(userId); - var enrollAccountUrl = Meteor.accounts.urls.enrollAccount(token); + var enrollAccountUrl = Accounts.urls.enrollAccount(token); Email.send({ to: email, - from: Meteor.accounts.emailTemplates.from, - subject: Meteor.accounts.emailTemplates.enrollAccount.subject(user), - text: Meteor.accounts.emailTemplates.enrollAccount.text(user, enrollAccountUrl) + from: Accounts.emailTemplates.from, + subject: Accounts.emailTemplates.enrollAccount.subject(user), + text: Accounts.emailTemplates.enrollAccount.text(user, enrollAccountUrl) }); }; // handler to login with password - Meteor.accounts.registerLoginHandler(function (options) { + Accounts.registerLoginHandler(function (options) { if (!options.srp) return undefined; // don't handle if (!options.srp.M) throw new Meteor.Error(400, "Must pass M in options.srp"); - var serialized = Meteor.accounts._srpChallenges.findOne( + var serialized = Accounts._srpChallenges.findOne( {M: options.srp.M}); if (!serialized) throw new Meteor.Error(403, "Incorrect password"); var userId = serialized.userId; - var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); + var loginToken = Accounts._loginTokens.insert({userId: userId}); // XXX we should remove srpChallenge documents from mongo, but we // need to make sure reconnects still work (meaning we can't @@ -274,8 +274,8 @@ // over the wire, it should only be run over SSL! // // Also, it might be nice if servers could turn this off. Or maybe it - // should be opt-in, not opt-out? Meteor.accounts.config option? - Meteor.accounts.registerLoginHandler(function (options) { + // should be opt-in, not opt-out? Accounts.config option? + Accounts.registerLoginHandler(function (options) { if (!options.password || !options.user) return undefined; // don't handle @@ -297,7 +297,7 @@ if (verifier.verifier !== newVerifier.verifier) throw new Meteor.Error(403, "Incorrect password"); - var loginToken = Meteor.accounts._loginTokens.insert({userId: user._id}); + var loginToken = Accounts._loginTokens.insert({userId: user._id}); return {token: loginToken, id: user._id}; }); @@ -352,7 +352,7 @@ if (email) user.emails = [{address: email, validated: false}]; - user = Meteor.accounts.onCreateUserHook(options, extra, user); + user = Accounts.onCreateUserHook(options, extra, user); var userId = Meteor.users.insert(user); return userId; }; @@ -360,7 +360,7 @@ // method for create user. Requests come from the client. Meteor.methods({ createUser: function (options, extra) { - if (Meteor.accounts._options.forbidSignups) + if (Accounts._options.forbidSignups) throw new Meteor.Error(403, "Signups forbidden"); var userId = createUser(options, extra); @@ -369,14 +369,14 @@ if (!userId) throw new Error("createUser failed to insert new user"); - // If `Meteor.accounts._options.validateEmails` is set, register + // If `Accounts._options.validateEmails` is set, register // a token to validate the user's primary email, and send it to // that address. - if (options.email && Meteor.accounts._options.validateEmails) - Meteor.accounts.sendValidationEmail(userId, options.email); + if (options.email && Accounts._options.validateEmails) + Accounts.sendValidationEmail(userId, options.email); // client gets logged in as the new user afterwards. - var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); + var loginToken = Accounts._loginTokens.insert({userId: userId}); this.setUserId(userId); return {token: loginToken, id: userId}; } @@ -413,7 +413,7 @@ user.services.password.srp)) { var email = user.emails[0].address; - Meteor.accounts.sendEnrollmentEmail(userId, email); + Accounts.sendEnrollmentEmail(userId, email); } return userId; diff --git a/packages/accounts-password/passwords_tests.js b/packages/accounts-password/passwords_tests.js index ee8757157d..00a2a4055a 100644 --- a/packages/accounts-password/passwords_tests.js +++ b/packages/accounts-password/passwords_tests.js @@ -95,7 +95,7 @@ if (Meteor.isClient) (function () { test.isTrue(result.id); test.isTrue(result.token); // emulate the real login behavior, so as not to confuse test. - Meteor.accounts.makeClientLoggedIn(result.id, result.token); + Accounts.makeClientLoggedIn(result.id, result.token); test.equal(Meteor.user().username, username); })); }, @@ -137,7 +137,7 @@ if (Meteor.isClient) (function () { test.isTrue(result.id); test.isTrue(result.token); // emulate the real login behavior, so as not to confuse test. - Meteor.accounts.makeClientLoggedIn(result.id, result.token); + Accounts.makeClientLoggedIn(result.id, result.token); test.equal(Meteor.user().username, username2); })); }, @@ -150,7 +150,7 @@ if (Meteor.isClient) (function () { })); }, logoutStep, - // test Meteor.accounts.validateNewUser + // test Accounts.validateNewUser function(test, expect) { Meteor.createUser({username: username3, password: password3}, {invalid: true}, // should fail the new user validators @@ -158,7 +158,7 @@ if (Meteor.isClient) (function () { test.equal(error.error, 403); })); }, - // test Meteor.accounts.onCreateUser + // test Accounts.onCreateUser function(test, expect) { Meteor.createUser({username: username3, password: password3}, {testOnCreateUserHook: true}, expect(function () { @@ -198,7 +198,7 @@ if (Meteor.isServer) (function () { 'passwords - setup more than one onCreateUserHook', function (test) { test.throws(function() { - Meteor.accounts.onCreateUser(function () {}); + Accounts.onCreateUser(function () {}); }); }); @@ -264,5 +264,5 @@ if (Meteor.isServer) (function () { }); }); - // XXX would be nice to test Meteor.accounts.config({forbidSignups: true}) + // XXX would be nice to test Accounts.config({forbidSignups: true}) }) (); diff --git a/packages/accounts-password/passwords_tests_setup.js b/packages/accounts-password/passwords_tests_setup.js index 5ffeae38a6..2b680b98e6 100644 --- a/packages/accounts-password/passwords_tests_setup.js +++ b/packages/accounts-password/passwords_tests_setup.js @@ -1,8 +1,8 @@ -Meteor.accounts.validateNewUser(function (user) { +Accounts.validateNewUser(function (user) { return !user.invalid; }); -Meteor.accounts.onCreateUser(function (options, extra, user) { +Accounts.onCreateUser(function (options, extra, user) { if (extra.testOnCreateUserHook) { user.profile = (user.profile || {}); user.profile.touchedByOnCreateUser = true; @@ -25,7 +25,7 @@ Meteor.accounts.onCreateUser(function (options, extra, user) { // // For now, we just test the one configuration state. You can comment // out each configuration option and see that the tests fail. -Meteor.accounts.config({ +Accounts.config({ validateEmails: true, // The 'accounts - updateOrCreateUser' test needs accounts without // usernames or emails, so we can't test with these on. diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js index e21a107452..147a72e79d 100644 --- a/packages/accounts-twitter/twitter_client.js +++ b/packages/accounts-twitter/twitter_client.js @@ -1,8 +1,8 @@ (function () { Meteor.loginWithTwitter = function (callback) { - var config = Meteor.accounts.configuration.findOne({service: 'twitter'}); + var config = Accounts.configuration.findOne({service: 'twitter'}); if (!config) { - callback && callback(new Meteor.accounts.ConfigError("Service not configured")); + callback && callback(new Accounts.ConfigError("Service not configured")); return; } @@ -21,7 +21,7 @@ + encodeURIComponent(callbackUrl) + '&state=' + state; - Meteor.accounts.oauth.initiateLogin(state, url, callback); + Accounts.oauth.initiateLogin(state, url, callback); }; })(); diff --git a/packages/accounts-twitter/twitter_common.js b/packages/accounts-twitter/twitter_common.js index 72d57689fa..3fdcd9d2bc 100644 --- a/packages/accounts-twitter/twitter_common.js +++ b/packages/accounts-twitter/twitter_common.js @@ -1,8 +1,8 @@ -if (!Meteor.accounts.twitter) { - Meteor.accounts.twitter = {}; +if (!Accounts.twitter) { + Accounts.twitter = {}; } -Meteor.accounts.twitter._urls = { +Accounts.twitter._urls = { requestToken: "https://api.twitter.com/oauth/request_token", authorize: "https://api.twitter.com/oauth/authorize", accessToken: "https://api.twitter.com/oauth/access_token", diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 900331d31b..6bddac2fba 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -1,6 +1,6 @@ (function () { - Meteor.accounts.oauth.registerService('twitter', 1, function(oauthBinding) { + Accounts.oauth.registerService('twitter', 1, function(oauthBinding) { var identity = oauthBinding.get('https://api.twitter.com/1/account/verify_credentials.json'); return { diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index 23c45781eb..193f99d301 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -13,7 +13,7 @@ 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 CONFIGURE_LOGIN_SERVICES_DIALOG_SAVE_ENABLED = "Accounts.facebook.saveEnabled"; var resetSession = function () { @@ -43,9 +43,9 @@ 'click #login-buttons-Facebook': function () { resetMessages(); Meteor.loginWithFacebook(function (e) { - if (!e || e instanceof Meteor.accounts.LoginCancelledError) { + if (!e || e instanceof Accounts.LoginCancelledError) { // do nothing - } else if (e instanceof Meteor.accounts.ConfigError) { + } else if (e instanceof Accounts.ConfigError) { configureService("Facebook"); // XXX refactor "Facebook" -> "facebook" } else { Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error"); @@ -56,9 +56,9 @@ 'click #login-buttons-Google': function () { resetMessages(); Meteor.loginWithGoogle(function (e) { - if (!e || e instanceof Meteor.accounts.LoginCancelledError) { + if (!e || e instanceof Accounts.LoginCancelledError) { // do nothing - } else if (e instanceof Meteor.accounts.ConfigError) { + } else if (e instanceof Accounts.ConfigError) { configureService("Google"); } else { Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error"); @@ -69,9 +69,9 @@ 'click #login-buttons-Weibo': function () { resetMessages(); Meteor.loginWithWeibo(function (e) { - if (!e || e instanceof Meteor.accounts.LoginCancelledError) { + if (!e || e instanceof Accounts.LoginCancelledError) { // do nothing - } else if (e instanceof Meteor.accounts.ConfigError) { + } else if (e instanceof Accounts.ConfigError) { configureService("Weibo"); } else { Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error"); @@ -82,9 +82,9 @@ 'click #login-buttons-Twitter': function () { resetMessages(); Meteor.loginWithTwitter(function (e) { - if (!e || e instanceof Meteor.accounts.LoginCancelledError) { + if (!e || e instanceof Accounts.LoginCancelledError) { // do nothing - } else if (e instanceof Meteor.accounts.ConfigError) { + } else if (e instanceof Accounts.ConfigError) { configureService("Twitter"); } else { Session.set(ERROR_MESSAGE_KEY, e.reason || "Unknown error"); @@ -115,7 +115,7 @@ }; Template.loginButtons.configurationLoaded = function () { - return Meteor.accounts.loginServicesConfigured(); + return Accounts.loginServicesConfigured(); }; Template.loginButtons.displayName = function () { @@ -211,17 +211,17 @@ var loginFields = [ {fieldName: 'username-or-email', fieldLabel: 'Username or Email', visible: function () { - return Meteor.accounts._options.requireUsername - && Meteor.accounts._options.requireEmail; + return Accounts._options.requireUsername + && Accounts._options.requireEmail; }}, {fieldName: 'username', fieldLabel: 'Username', visible: function () { - return Meteor.accounts._options.requireUsername - && !Meteor.accounts._options.requireEmail; + return Accounts._options.requireUsername + && !Accounts._options.requireEmail; }}, {fieldName: 'email', fieldLabel: 'Email', visible: function () { - return !Meteor.accounts._options.requireUsername; + return !Accounts._options.requireUsername; }}, {fieldName: 'password', fieldLabel: 'Password', inputType: 'password', visible: function () { @@ -232,12 +232,12 @@ var signupFields = [ {fieldName: 'username', fieldLabel: 'Username', visible: function () { - return Meteor.accounts._options.requireUsername; + return Accounts._options.requireUsername; }}, {fieldName: 'email', fieldLabel: 'Email', visible: function () { - return !Meteor.accounts._options.requireUsername - || Meteor.accounts._options.requireEmail; + return !Accounts._options.requireUsername + || Accounts._options.requireEmail; }}, {fieldName: 'password', fieldLabel: 'Password', inputType: 'password', visible: function () { @@ -246,8 +246,8 @@ {fieldName: 'password-again', fieldLabel: 'Password (again)', inputType: 'password', visible: function () { - return Meteor.accounts._options.requireUsername - && !Meteor.accounts._options.requireEmail; + return Accounts._options.requireUsername + && !Accounts._options.requireEmail; }} ]; @@ -282,12 +282,12 @@ }; Template.loginButtonsServicesRow.showForgotPasswordLink = function () { - return Meteor.accounts._options.requireEmail - || !Meteor.accounts._options.requireUsername; + return Accounts._options.requireEmail + || !Accounts._options.requireUsername; }; Template.loginButtonsServicesRow.configured = function () { - return !!Meteor.accounts.configuration.findOne({service: this.name.toLowerCase()}); + return !!Accounts.configuration.findOne({service: this.name.toLowerCase()}); }; @@ -381,7 +381,7 @@ }, 'click #login-buttons-cancel-reset-password': function () { Session.set(RESET_PASSWORD_TOKEN_KEY, null); - Meteor.accounts._enableAutoLogin(); + Accounts._enableAutoLogin(); } }; @@ -398,7 +398,7 @@ Session.set(ERROR_MESSAGE_KEY, error.reason || "Unknown error"); } else { Session.set(RESET_PASSWORD_TOKEN_KEY, null); - Meteor.accounts._enableAutoLogin(); + Accounts._enableAutoLogin(); } }); }; @@ -407,8 +407,8 @@ return Session.get(RESET_PASSWORD_TOKEN_KEY); }; - if (Meteor.accounts._resetPasswordToken) { - Session.set(RESET_PASSWORD_TOKEN_KEY, Meteor.accounts._resetPasswordToken); + if (Accounts._resetPasswordToken) { + Session.set(RESET_PASSWORD_TOKEN_KEY, Accounts._resetPasswordToken); } @@ -426,7 +426,7 @@ }, 'click #login-buttons-cancel-enroll-account': function () { Session.set(ENROLL_ACCOUNT_TOKEN_KEY, null); - Meteor.accounts._enableAutoLogin(); + Accounts._enableAutoLogin(); } }; @@ -443,7 +443,7 @@ Session.set(ERROR_MESSAGE_KEY, error.reason || "Unknown error"); } else { Session.set(ENROLL_ACCOUNT_TOKEN_KEY, null); - Meteor.accounts._enableAutoLogin(); + Accounts._enableAutoLogin(); } }); }; @@ -452,8 +452,8 @@ return Session.get(ENROLL_ACCOUNT_TOKEN_KEY); }; - if (Meteor.accounts._enrollAccountToken) { - Session.set(ENROLL_ACCOUNT_TOKEN_KEY, Meteor.accounts._enrollAccountToken); + if (Accounts._enrollAccountToken) { + Session.set(ENROLL_ACCOUNT_TOKEN_KEY, Accounts._enrollAccountToken); } @@ -476,9 +476,9 @@ // issue. We can't be sure that accounts-password is loaded earlier // than accounts-ui so Meteor.validateEmail might not be defined. Meteor.startup(function () { - if (Meteor.accounts._validateEmailToken) { - Meteor.validateEmail(Meteor.accounts._validateEmailToken, function(error) { - Meteor.accounts._enableAutoLogin(); + if (Accounts._validateEmailToken) { + Meteor.validateEmail(Accounts._validateEmailToken, function(error) { + Accounts._enableAutoLogin(); if (!error) Session.set(JUST_VALIDATED_USER_KEY, true); // XXX show something if there was an error. @@ -645,7 +645,7 @@ } } - if (Meteor.accounts._options.validateEmails) + if (Accounts._options.validateEmails) options.validation = true; Meteor.createUser(options, function (error) { @@ -665,19 +665,19 @@ var getLoginServices = 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) + // list of services, such as _.each(Accounts.services, ...) + if (Accounts.facebook) ret.push({name: 'Facebook'}); - if (Meteor.accounts.google) + if (Accounts.google) ret.push({name: 'Google'}); - if (Meteor.accounts.weibo) + if (Accounts.weibo) ret.push({name: 'Weibo'}); - if (Meteor.accounts.twitter) + if (Accounts.twitter) ret.push({name: 'Twitter'}); // make sure to put accounts last, since this is the order in the // ui as well - if (Meteor.accounts.passwords) + if (Accounts.passwords) ret.push({name: 'Password'}); return ret; diff --git a/packages/accounts-urls/url_client.js b/packages/accounts-urls/url_client.js index dda6b80f2f..47add8934d 100644 --- a/packages/accounts-urls/url_client.js +++ b/packages/accounts-urls/url_client.js @@ -1,6 +1,6 @@ (function () { - if (!Meteor.accounts) - Meteor.accounts = {}; + if (!Accounts) + Accounts = {}; // reads a reset password token from the url's hash fragment, if it's // there. if so prevent automatically logging in since it could be @@ -13,8 +13,8 @@ var match; match = window.location.hash.match(/^\#\?reset-password\/(.*)$/); if (match) { - Meteor.accounts._preventAutoLogin = true; - Meteor.accounts._resetPasswordToken = match[1]; + Accounts._preventAutoLogin = true; + Accounts._resetPasswordToken = match[1]; window.location.hash = ''; } @@ -30,8 +30,8 @@ // in line with the hash fragment approach) match = window.location.hash.match(/^\#\?validate-email\/(.*)$/); if (match) { - Meteor.accounts._preventAutoLogin = true; - Meteor.accounts._validateEmailToken = match[1]; + Accounts._preventAutoLogin = true; + Accounts._validateEmailToken = match[1]; window.location.hash = ''; } @@ -40,8 +40,8 @@ // reset password links. match = window.location.hash.match(/^\#\?enroll-account\/(.*)$/); if (match) { - Meteor.accounts._preventAutoLogin = true; - Meteor.accounts._enrollAccountToken = match[1]; + Accounts._preventAutoLogin = true; + Accounts._enrollAccountToken = match[1]; window.location.hash = ''; } })(); diff --git a/packages/accounts-urls/url_server.js b/packages/accounts-urls/url_server.js index 2b8e991a58..cf82b12375 100644 --- a/packages/accounts-urls/url_server.js +++ b/packages/accounts-urls/url_server.js @@ -1,17 +1,17 @@ -if (!Meteor.accounts) - Meteor.accounts = {}; +if (!Accounts) + Accounts = {}; -if (!Meteor.accounts.urls) - Meteor.accounts.urls = {}; +if (!Accounts.urls) + Accounts.urls = {}; -Meteor.accounts.urls.resetPassword = function (token) { +Accounts.urls.resetPassword = function (token) { return Meteor.absoluteUrl('#?reset-password/' + token); }; -Meteor.accounts.urls.validateEmail = function (token) { +Accounts.urls.validateEmail = function (token) { return Meteor.absoluteUrl('#?validate-email/' + token); }; -Meteor.accounts.urls.enrollAccount = function (token) { +Accounts.urls.enrollAccount = function (token) { return Meteor.absoluteUrl('#?enroll-account/' + token); }; diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js index 9cc2bfaf94..e119a13e9f 100644 --- a/packages/accounts-weibo/weibo_client.js +++ b/packages/accounts-weibo/weibo_client.js @@ -1,8 +1,8 @@ (function () { Meteor.loginWithWeibo = function (callback) { - var config = Meteor.accounts.configuration.findOne({service: 'weibo'}); + var config = Accounts.configuration.findOne({service: 'weibo'}); if (!config) { - callback && callback(new Meteor.accounts.ConfigError("Service not configured")); + callback && callback(new Accounts.ConfigError("Service not configured")); return; } @@ -15,7 +15,7 @@ '&redirect_uri=' + Meteor.absoluteUrl('_oauth/weibo?close', {replaceLocalhost: true}) + '&state=' + state; - Meteor.accounts.oauth.initiateLogin(state, loginUrl, callback); + Accounts.oauth.initiateLogin(state, loginUrl, callback); }; }) (); diff --git a/packages/accounts-weibo/weibo_common.js b/packages/accounts-weibo/weibo_common.js index 9f9d3f0692..c655866943 100644 --- a/packages/accounts-weibo/weibo_common.js +++ b/packages/accounts-weibo/weibo_common.js @@ -1,9 +1,9 @@ -if (!Meteor.accounts.weibo) { - Meteor.accounts.weibo = {}; - Meteor.accounts.weibo._requireConfigs = ['_clientId', '_appUrl']; +if (!Accounts.weibo) { + Accounts.weibo = {}; + Accounts.weibo._requireConfigs = ['_clientId', '_appUrl']; } -Meteor.accounts.weibo.config = function(clientId, appUrl) { - Meteor.accounts.weibo._clientId = clientId; - Meteor.accounts.weibo._appUrl = appUrl; +Accounts.weibo.config = function(clientId, appUrl) { + Accounts.weibo._clientId = clientId; + Accounts.weibo._appUrl = appUrl; }; diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index 1c95214001..c8d8e8b882 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -1,6 +1,6 @@ (function () { - Meteor.accounts.oauth.registerService('weibo', 2, function(query) { + Accounts.oauth.registerService('weibo', 2, function(query) { var accessToken = getAccessToken(query); var identity = getIdentity(accessToken.access_token, parseInt(accessToken.uid, 10)); @@ -20,9 +20,9 @@ }); var getAccessToken = function (query) { - var config = Meteor.accounts.configuration.findOne({service: 'weibo'}); + var config = Accounts.configuration.findOne({service: 'weibo'}); if (!config) - throw new Meteor.accounts.ConfigError("Service not configured"); + throw new Accounts.ConfigError("Service not configured"); var result = Meteor.http.post( "https://api.weibo.com/oauth2/access_token", {params: { From 75c69301c0e61df2e8f47190a6a6615a26e0cce1 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 1 Oct 2012 13:53:42 -0700 Subject: [PATCH 162/239] Fixup Accounts initialization, which broke on rename. --- packages/accounts-base/accounts_common.js | 3 +-- packages/accounts-urls/url_client.js | 2 +- packages/accounts-urls/url_server.js | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index d3ae490a5a..5a4b951c69 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -1,6 +1,5 @@ -if (!Accounts) { +if (typeof Accounts === 'undefined') Accounts = {}; -} if (!Accounts._options) { Accounts._options = {}; diff --git a/packages/accounts-urls/url_client.js b/packages/accounts-urls/url_client.js index 47add8934d..2d3f809e4d 100644 --- a/packages/accounts-urls/url_client.js +++ b/packages/accounts-urls/url_client.js @@ -1,5 +1,5 @@ (function () { - if (!Accounts) + if (typeof Accounts === 'undefined') Accounts = {}; // reads a reset password token from the url's hash fragment, if it's diff --git a/packages/accounts-urls/url_server.js b/packages/accounts-urls/url_server.js index cf82b12375..55a5cfa2b8 100644 --- a/packages/accounts-urls/url_server.js +++ b/packages/accounts-urls/url_server.js @@ -1,4 +1,4 @@ -if (!Accounts) +if (typeof Accounts === 'undefined') Accounts = {}; if (!Accounts.urls) From 9392fbb51dddbbc96c6000e31f94535541eb6bf9 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 1 Oct 2012 15:44:14 -0700 Subject: [PATCH 163/239] Prepend '_' to internal things in Accounts. find . -name '*.js' -print0 | xargs -0 perl -pi -e 's/makeClientLoggedIn/_makeClientLoggedIn/g' find . -name '*.js' -print0 | xargs -0 perl -pi -e 's/makeClientLoggedOut/_makeClientLoggedOut/g' find . -name '*.js' -print0 | xargs -0 perl -pi -e 's/storeLoginToken/_storeLoginToken/g' find . -name '*.js' -print0 | xargs -0 perl -pi -e 's/storedLoginToken/_storedLoginToken/g' find . -name '*.js' -print0 | xargs -0 perl -pi -e 's/unstoreLoginToken/_unstoreLoginToken/g' find . -name '*.js' -print0 | xargs -0 perl -pi -e 's/storedUserId/_storedUserId/g' find . -name '*.js' -print0 | xargs -0 perl -pi -e 's/un_storeLoginToken/_unstoreLoginToken/g' --- packages/accounts-base/accounts_client.js | 2 +- packages/accounts-base/localstorage_token.js | 28 +++++++++---------- .../accounts-oauth-helper/oauth_client.js | 2 +- .../accounts-password/passwords_client.js | 6 ++-- packages/accounts-password/passwords_tests.js | 4 +-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/accounts-base/accounts_client.js b/packages/accounts-base/accounts_client.js index c894b10f50..a4249876b9 100644 --- a/packages/accounts-base/accounts_client.js +++ b/packages/accounts-base/accounts_client.js @@ -26,7 +26,7 @@ if (error) { callback && callback(error); } else { - Accounts.makeClientLoggedOut(); + Accounts._makeClientLoggedOut(); callback && callback(); } }); diff --git a/packages/accounts-base/localstorage_token.js b/packages/accounts-base/localstorage_token.js index 43391a88de..51ad0ac597 100644 --- a/packages/accounts-base/localstorage_token.js +++ b/packages/accounts-base/localstorage_token.js @@ -3,7 +3,7 @@ var loginTokenKey = "Meteor.loginToken"; var userIdKey = "Meteor.userId"; - Accounts.storeLoginToken = function(userId, token) { + Accounts._storeLoginToken = function(userId, token) { localStorage.setItem(userIdKey, userId); localStorage.setItem(loginTokenKey, token); @@ -12,7 +12,7 @@ Accounts._lastLoginTokenWhenPolled = token; }; - Accounts.unstoreLoginToken = function() { + Accounts._unstoreLoginToken = function() { localStorage.removeItem(userIdKey); localStorage.removeItem(loginTokenKey); @@ -21,27 +21,27 @@ Accounts._lastLoginTokenWhenPolled = null; }; - Accounts.storedLoginToken = function() { + Accounts._storedLoginToken = function() { return localStorage.getItem(loginTokenKey); }; - Accounts.storedUserId = function() { + Accounts._storedUserId = function() { return localStorage.getItem(userIdKey); }; - Accounts.makeClientLoggedOut = function() { - Accounts.unstoreLoginToken(); + Accounts._makeClientLoggedOut = function() { + Accounts._unstoreLoginToken(); Meteor.default_connection.setUserId(null); Meteor.default_connection.onReconnect = null; }; - Accounts.makeClientLoggedIn = function(userId, token) { - Accounts.storeLoginToken(userId, token); + Accounts._makeClientLoggedIn = function(userId, token) { + Accounts._storeLoginToken(userId, token); Meteor.default_connection.setUserId(userId); Meteor.default_connection.onReconnect = function() { Meteor.apply('login', [{resume: token}], {wait: true}, function(error, result) { if (error) { - Accounts.makeClientLoggedOut(); + Accounts._makeClientLoggedOut(); throw error; } else { // nothing to do @@ -62,21 +62,21 @@ Meteor.loginWithToken = function (token, errorCallback) { throw error; } - Accounts.makeClientLoggedIn(result.id, result.token); + Accounts._makeClientLoggedIn(result.id, result.token); }); }; if (!Accounts._preventAutoLogin) { // Immediately try to log in via local storage, so that any DDP // messages are sent after we have established our user account - var token = Accounts.storedLoginToken(); + var token = Accounts._storedLoginToken(); if (token) { // On startup, optimistically present us as logged in while the // request is in flight. This reduces page flicker on startup. - var userId = Accounts.storedUserId(); + var userId = Accounts._storedUserId(); userId && Meteor.default_connection.setUserId(userId); Meteor.loginWithToken(token, function () { - Accounts.makeClientLoggedOut(); + Accounts._makeClientLoggedOut(); }); } } @@ -88,7 +88,7 @@ Accounts._pollStoredLoginToken = function() { if (Accounts._preventAutoLogin) return; - var currentLoginToken = Accounts.storedLoginToken(); + var currentLoginToken = Accounts._storedLoginToken(); // != instead of !== just to make sure undefined and null are treated the same if (Accounts._lastLoginTokenWhenPolled != currentLoginToken) { diff --git a/packages/accounts-oauth-helper/oauth_client.js b/packages/accounts-oauth-helper/oauth_client.js index 16dfdacbff..e8d96a3fc9 100644 --- a/packages/accounts-oauth-helper/oauth_client.js +++ b/packages/accounts-oauth-helper/oauth_client.js @@ -47,7 +47,7 @@ callback && callback(new Accounts.LoginCancelledError("Popup closed")); } else { - Accounts.makeClientLoggedIn(result.id, result.token); + Accounts._makeClientLoggedIn(result.id, result.token); callback && callback(); } }); diff --git a/packages/accounts-password/passwords_client.js b/packages/accounts-password/passwords_client.js index ab4f7b09ab..ac89f20085 100644 --- a/packages/accounts-password/passwords_client.js +++ b/packages/accounts-password/passwords_client.js @@ -22,7 +22,7 @@ return; } - Accounts.makeClientLoggedIn(result.id, result.token); + Accounts._makeClientLoggedIn(result.id, result.token); callback && callback(undefined, {message: 'Success'}); }); }; @@ -68,7 +68,7 @@ return; } - Accounts.makeClientLoggedIn(result.id, result.token); + Accounts._makeClientLoggedIn(result.id, result.token); callback && callback(); }); }); @@ -158,7 +158,7 @@ callback && callback(error); } - Accounts.makeClientLoggedIn(result.id, result.token); + Accounts._makeClientLoggedIn(result.id, result.token); callback && callback(); }); }; diff --git a/packages/accounts-password/passwords_tests.js b/packages/accounts-password/passwords_tests.js index 00a2a4055a..6933d0ac40 100644 --- a/packages/accounts-password/passwords_tests.js +++ b/packages/accounts-password/passwords_tests.js @@ -95,7 +95,7 @@ if (Meteor.isClient) (function () { test.isTrue(result.id); test.isTrue(result.token); // emulate the real login behavior, so as not to confuse test. - Accounts.makeClientLoggedIn(result.id, result.token); + Accounts._makeClientLoggedIn(result.id, result.token); test.equal(Meteor.user().username, username); })); }, @@ -137,7 +137,7 @@ if (Meteor.isClient) (function () { test.isTrue(result.id); test.isTrue(result.token); // emulate the real login behavior, so as not to confuse test. - Accounts.makeClientLoggedIn(result.id, result.token); + Accounts._makeClientLoggedIn(result.id, result.token); test.equal(Meteor.user().username, username2); })); }, From 0e5b8083b5ffd11bec06494eb8068d174d954451 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 1 Oct 2012 15:49:39 -0700 Subject: [PATCH 164/239] Meteor.changePassword -> Accounts.changePassword --- packages/accounts-password/passwords_client.js | 2 +- packages/accounts-password/passwords_tests.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/accounts-password/passwords_client.js b/packages/accounts-password/passwords_client.js index ac89f20085..6912827985 100644 --- a/packages/accounts-password/passwords_client.js +++ b/packages/accounts-password/passwords_client.js @@ -78,7 +78,7 @@ // @param oldPassword {String|null} // @param newPassword {String} // @param callback {Function(error|undefined)} - Meteor.changePassword = function (oldPassword, newPassword, callback) { + Accounts.changePassword = function (oldPassword, newPassword, callback) { if (!Meteor.user()) { callback && callback(new Error("Must be logged in to change password.")); return; diff --git a/packages/accounts-password/passwords_tests.js b/packages/accounts-password/passwords_tests.js index 6933d0ac40..40bd1eb8d7 100644 --- a/packages/accounts-password/passwords_tests.js +++ b/packages/accounts-password/passwords_tests.js @@ -101,14 +101,14 @@ if (Meteor.isClient) (function () { }, // change password with bad old password. function (test, expect) { - Meteor.changePassword(password2, password2, expect(function (error) { + Accounts.changePassword(password2, password2, expect(function (error) { test.isTrue(error); test.equal(Meteor.user().username, username); })); }, // change password with good old password. function (test, expect) { - Meteor.changePassword(password, password2, expect(function (error) { + Accounts.changePassword(password, password2, expect(function (error) { test.equal(error, undefined); test.equal(Meteor.user().username, username); })); From 7cbd9fd243824d377958897dc04221789f459901 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 1 Oct 2012 15:54:55 -0700 Subject: [PATCH 165/239] Meteor.createUser -> Accounts.createUser --- packages/accounts-password/email_tests.js | 4 ++-- packages/accounts-password/email_tests_setup.js | 2 +- packages/accounts-password/passwords_client.js | 2 +- packages/accounts-password/passwords_server.js | 2 +- packages/accounts-password/passwords_tests.js | 16 ++++++++-------- packages/accounts-ui-unstyled/login_buttons.js | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/accounts-password/email_tests.js b/packages/accounts-password/email_tests.js index 526f25b1c1..06394753b4 100644 --- a/packages/accounts-password/email_tests.js +++ b/packages/accounts-password/email_tests.js @@ -14,7 +14,7 @@ testAsyncMulti("accounts emails - reset password flow", [ function (test, expect) { email1 = Meteor.uuid() + "-intercept@example.com"; - Meteor.createUser({email: email1, password: 'foobar'}, + Accounts.createUser({email: email1, password: 'foobar'}, expect(function (error) { test.equal(error, undefined); })); @@ -81,7 +81,7 @@ function (test, expect) { email2 = Meteor.uuid() + "-intercept@example.com"; email3 = Meteor.uuid() + "-intercept@example.com"; - Meteor.createUser( + Accounts.createUser( {email: email2, password: 'foobar'}, expect(function (error) { test.equal(error, undefined); diff --git a/packages/accounts-password/email_tests_setup.js b/packages/accounts-password/email_tests_setup.js index c932f62a53..9159fbcc65 100644 --- a/packages/accounts-password/email_tests_setup.js +++ b/packages/accounts-password/email_tests_setup.js @@ -32,7 +32,7 @@ }, createUserOnServer: function (email) { - var userId = Meteor.createUser({email: email}); + var userId = Accounts.createUser({email: email}); return Meteor.users.findOne(userId); } }); diff --git a/packages/accounts-password/passwords_client.js b/packages/accounts-password/passwords_client.js index 6912827985..fd9cba5222 100644 --- a/packages/accounts-password/passwords_client.js +++ b/packages/accounts-password/passwords_client.js @@ -1,5 +1,5 @@ (function () { - Meteor.createUser = function (options, extra, callback) { + Accounts.createUser = function (options, extra, callback) { options = _.clone(options); // we'll be modifying options if (typeof extra === "function") { diff --git a/packages/accounts-password/passwords_server.js b/packages/accounts-password/passwords_server.js index e771835dde..9cc2725975 100644 --- a/packages/accounts-password/passwords_server.js +++ b/packages/accounts-password/passwords_server.js @@ -388,7 +388,7 @@ // after creation. // // returns userId or throws an error if it can't create - Meteor.createUser = function (options, extra, callback) { + Accounts.createUser = function (options, extra, callback) { if (typeof extra === "function") { callback = extra; diff --git a/packages/accounts-password/passwords_tests.js b/packages/accounts-password/passwords_tests.js index 40bd1eb8d7..a87fb97071 100644 --- a/packages/accounts-password/passwords_tests.js +++ b/packages/accounts-password/passwords_tests.js @@ -40,7 +40,7 @@ if (Meteor.isClient) (function () { var quiesceCallback = expect(function () { test.equal(Meteor.user().username, username); }); - Meteor.createUser({username: username, email: email, password: password}, + Accounts.createUser({username: username, email: email, password: password}, expect(function (error) { test.equal(error, undefined); Meteor.default_connection.onQuiesce(quiesceCallback); @@ -152,7 +152,7 @@ if (Meteor.isClient) (function () { logoutStep, // test Accounts.validateNewUser function(test, expect) { - Meteor.createUser({username: username3, password: password3}, + Accounts.createUser({username: username3, password: password3}, {invalid: true}, // should fail the new user validators expect(function (error) { test.equal(error.error, 403); @@ -160,7 +160,7 @@ if (Meteor.isClient) (function () { }, // test Accounts.onCreateUser function(test, expect) { - Meteor.createUser({username: username3, password: password3}, + Accounts.createUser({username: username3, password: password3}, {testOnCreateUserHook: true}, expect(function () { test.equal(Meteor.user().profile.touchedByOnCreateUser, true); })); @@ -208,15 +208,15 @@ if (Meteor.isServer) (function () { function (test) { var email = Meteor.uuid() + '@example.com'; test.throws(function () { - Meteor.createUser({email: email}, - {invalid: true}); // should fail the new user validators + Accounts.createUser({email: email}, + {invalid: true}); // should fail the new user validators }); // disable sending emails var oldEmailSend = Email.send; Email.send = function() {}; - var userId = Meteor.createUser({email: email}, - {testOnCreateUserHook: true}); + var userId = Accounts.createUser({email: email}, + {testOnCreateUserHook: true}); Email.send = oldEmailSend; test.isTrue(userId); @@ -230,7 +230,7 @@ if (Meteor.isServer) (function () { function (test) { var username = Meteor.uuid(); - var userId = Meteor.createUser({username: username}, {}); + var userId = Accounts.createUser({username: username}, {}); var user = Meteor.users.findOne(userId); // no services yet. diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index 193f99d301..23eb87e7f0 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -648,7 +648,7 @@ if (Accounts._options.validateEmails) options.validation = true; - Meteor.createUser(options, function (error) { + Accounts.createUser(options, function (error) { if (error) { Session.set(ERROR_MESSAGE_KEY, error.reason || "Unknown error"); } From 3d791cce6bb5dc51b9249c4258e1d349d668778a Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 1 Oct 2012 15:57:15 -0700 Subject: [PATCH 166/239] Meteor.forgotPassword -> Accounts.forgotPassword. --- packages/accounts-password/email_tests.js | 2 +- packages/accounts-password/passwords_client.js | 2 +- packages/accounts-ui-unstyled/login_buttons.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/accounts-password/email_tests.js b/packages/accounts-password/email_tests.js index 06394753b4..60781b2f40 100644 --- a/packages/accounts-password/email_tests.js +++ b/packages/accounts-password/email_tests.js @@ -20,7 +20,7 @@ })); }, function (test, expect) { - Meteor.forgotPassword({email: email1}, expect(function (error) { + Accounts.forgotPassword({email: email1}, expect(function (error) { test.equal(error, undefined); })); }, diff --git a/packages/accounts-password/passwords_client.js b/packages/accounts-password/passwords_client.js index fd9cba5222..a5f0888424 100644 --- a/packages/accounts-password/passwords_client.js +++ b/packages/accounts-password/passwords_client.js @@ -131,7 +131,7 @@ // @param options {Object} // - email: (email) // @param callback (optional) {Function(error|undefined)} - Meteor.forgotPassword = function(options, callback) { + Accounts.forgotPassword = function(options, callback) { if (!options.email) throw new Error("Must pass options.email"); Meteor.call("forgotPassword", options, callback); diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index 23eb87e7f0..6641400fc3 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -322,7 +322,7 @@ var email = document.getElementById("forgot-password-email").value; if (email.indexOf('@') !== -1) { - Meteor.forgotPassword({email: email}, function (error) { + Accounts.forgotPassword({email: email}, function (error) { if (error) Session.set(ERROR_MESSAGE_KEY, error.reason || "Unknown error"); else From dd640729ff8d9ec3548c6e693c656b11ead89c14 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 1 Oct 2012 16:12:59 -0700 Subject: [PATCH 167/239] Meteor.resetPassword -> Accounts.resetPassword. --- packages/accounts-password/email_tests.js | 4 ++-- packages/accounts-password/passwords_client.js | 2 +- packages/accounts-ui-unstyled/login_buttons.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/accounts-password/email_tests.js b/packages/accounts-password/email_tests.js index 60781b2f40..c5a57c9698 100644 --- a/packages/accounts-password/email_tests.js +++ b/packages/accounts-password/email_tests.js @@ -38,7 +38,7 @@ })); }, function (test, expect) { - Meteor.resetPassword(resetPasswordToken, "newPassword", expect(function(error) { + Accounts.resetPassword(resetPasswordToken, "newPassword", expect(function(error) { test.isFalse(error); })); }, @@ -172,7 +172,7 @@ getEnrollAccountToken(email4, test, expect); }, function (test, expect) { - Meteor.resetPassword(enrollAccountToken, 'password', expect(function(error) { + Accounts.resetPassword(enrollAccountToken, 'password', expect(function(error) { test.isFalse(error); })); }, diff --git a/packages/accounts-password/passwords_client.js b/packages/accounts-password/passwords_client.js index a5f0888424..bb91131117 100644 --- a/packages/accounts-password/passwords_client.js +++ b/packages/accounts-password/passwords_client.js @@ -143,7 +143,7 @@ // @param token {String} // @param newPassword {String} // @param callback (optional) {Function(error|undefined)} - Meteor.resetPassword = function(token, newPassword, callback) { + Accounts.resetPassword = function(token, newPassword, callback) { if (!token) throw new Error("Need to pass token"); if (!newPassword) diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index 6641400fc3..51bda4782e 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -391,7 +391,7 @@ if (!validatePassword(newPassword)) return; - Meteor.resetPassword( + Accounts.resetPassword( Session.get(RESET_PASSWORD_TOKEN_KEY), newPassword, function (error) { if (error) { @@ -436,7 +436,7 @@ if (!validatePassword(password)) return; - Meteor.resetPassword( + Accounts.resetPassword( Session.get(ENROLL_ACCOUNT_TOKEN_KEY), password, function (error) { if (error) { From ccb79ff0d9df92a494dd97850d20949774f466d9 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 1 Oct 2012 16:20:57 -0700 Subject: [PATCH 168/239] Meteor.validateEmail -> Accounts.validateEmail --- packages/accounts-password/email_tests.js | 4 ++-- packages/accounts-password/passwords_client.js | 2 +- packages/accounts-ui-unstyled/login_buttons.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/accounts-password/email_tests.js b/packages/accounts-password/email_tests.js index c5a57c9698..e657270617 100644 --- a/packages/accounts-password/email_tests.js +++ b/packages/accounts-password/email_tests.js @@ -96,7 +96,7 @@ getValidateEmailToken(email2, test, expect); }, function (test, expect) { - Meteor.validateEmail(validateEmailToken, expect(function(error) { + Accounts.validateEmail(validateEmailToken, expect(function(error) { test.isFalse(error); })); // ARGH! ON QUIESCE!! @@ -124,7 +124,7 @@ getValidateEmailToken(email3, test, expect); }, function (test, expect) { - Meteor.validateEmail(validateEmailToken, expect(function(error) { + Accounts.validateEmail(validateEmailToken, expect(function(error) { test.isFalse(error); })); }, diff --git a/packages/accounts-password/passwords_client.js b/packages/accounts-password/passwords_client.js index bb91131117..3cc22f82c0 100644 --- a/packages/accounts-password/passwords_client.js +++ b/packages/accounts-password/passwords_client.js @@ -168,7 +168,7 @@ // // @param token {String} // @param callback (optional) {Function(error|undefined)} - Meteor.validateEmail = function(token, callback) { + Accounts.validateEmail = function(token, callback) { if (!token) throw new Error("Need to pass token"); diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index 51bda4782e..20da5dbf02 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -474,10 +474,10 @@ // Needs to be in Meteor.startup because of a package loading order // issue. We can't be sure that accounts-password is loaded earlier - // than accounts-ui so Meteor.validateEmail might not be defined. + // than accounts-ui so Accounts.validateEmail might not be defined. Meteor.startup(function () { if (Accounts._validateEmailToken) { - Meteor.validateEmail(Accounts._validateEmailToken, function(error) { + Accounts.validateEmail(Accounts._validateEmailToken, function(error) { Accounts._enableAutoLogin(); if (!error) Session.set(JUST_VALIDATED_USER_KEY, true); From 12934c96a99bc64bdaa57b6b5a0bb9548413d617 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 2 Oct 2012 22:15:54 -0700 Subject: [PATCH 169/239] Feedback from review. --- examples/todos/accounts/config.js | 5 ----- examples/todos/accounts/server/secrets.js | 5 ----- examples/todos/accounts/services.js | 5 ----- packages/accounts-google/google_server.js | 4 ---- packages/accounts-ui-unstyled/login_buttons.js | 2 +- packages/accounts-weibo/weibo_common.js | 6 ------ 6 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 examples/todos/accounts/config.js delete mode 100644 examples/todos/accounts/server/secrets.js delete mode 100644 examples/todos/accounts/services.js diff --git a/examples/todos/accounts/config.js b/examples/todos/accounts/config.js deleted file mode 100644 index cde224a4dd..0000000000 --- a/examples/todos/accounts/config.js +++ /dev/null @@ -1,5 +0,0 @@ -Accounts.config({ - requireEmail: false, - requireUsername: false, - validateEmails: true -}); diff --git a/examples/todos/accounts/server/secrets.js b/examples/todos/accounts/server/secrets.js deleted file mode 100644 index 86792b48ff..0000000000 --- a/examples/todos/accounts/server/secrets.js +++ /dev/null @@ -1,5 +0,0 @@ -// Modify and uncomment the following lines to configure login services. -// Also see accounts/services.js - -// Accounts.facebook.setSecret('SECRET'); -// Accounts.google.setSecret('SECRET'); diff --git a/examples/todos/accounts/services.js b/examples/todos/accounts/services.js deleted file mode 100644 index 8e2ad94ddb..0000000000 --- a/examples/todos/accounts/services.js +++ /dev/null @@ -1,5 +0,0 @@ -// Modify and uncomment the following lines to configure login services. -// Also see accounts/server/secrets.js - -// Accounts.facebook.config('218833638237574', 'http://auth-todos.meteor.com'); -// Accounts.google.config('987846107089.apps.googleusercontent.com', 'http://auth-todos.meteor.com'); diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 8da55b0fb4..51a0e27018 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -1,9 +1,5 @@ (function () { - Accounts.google.setSecret = function (secret) { - Accounts.google._secret = secret; - }; - Accounts.oauth.registerService('google', 2, function(query) { var accessToken = getAccessToken(query); diff --git a/packages/accounts-ui-unstyled/login_buttons.js b/packages/accounts-ui-unstyled/login_buttons.js index 20da5dbf02..a6204fe778 100644 --- a/packages/accounts-ui-unstyled/login_buttons.js +++ b/packages/accounts-ui-unstyled/login_buttons.js @@ -13,7 +13,7 @@ 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 = "Accounts.facebook.saveEnabled"; + var CONFIGURE_LOGIN_SERVICES_DIALOG_SAVE_ENABLED = "Meteor.loginButtons.saveEnabled"; var resetSession = function () { diff --git a/packages/accounts-weibo/weibo_common.js b/packages/accounts-weibo/weibo_common.js index c655866943..19ec575ef6 100644 --- a/packages/accounts-weibo/weibo_common.js +++ b/packages/accounts-weibo/weibo_common.js @@ -1,9 +1,3 @@ if (!Accounts.weibo) { Accounts.weibo = {}; - Accounts.weibo._requireConfigs = ['_clientId', '_appUrl']; } - -Accounts.weibo.config = function(clientId, appUrl) { - Accounts.weibo._clientId = clientId; - Accounts.weibo._appUrl = appUrl; -}; From 7490854b64cd7e20cf491c4fb8c5b7c612e5bf70 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 3 Oct 2012 16:38:47 -0700 Subject: [PATCH 170/239] Better error message when you try to use Meteor.userId() outside a method. --- packages/accounts-base/accounts_server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index d687ff58a3..b617a92c97 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -88,7 +88,7 @@ // record changes. var currentInvocation = Meteor._CurrentInvocation.get(); if (!currentInvocation || !currentInvocation.userId) - throw new Error("Meteor.userId can only be invoked in method calls."); + throw new Error("Meteor.userId can only be invoked in method calls. Use this.userId() in publish functions."); return currentInvocation.userId(); }; From 507792523a6c99a19f6ba6057aa7c7d54d801102 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 3 Oct 2012 16:50:13 -0700 Subject: [PATCH 171/239] Fix bug where validating your email did not log you in. --- packages/accounts-password/passwords_client.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/accounts-password/passwords_client.js b/packages/accounts-password/passwords_client.js index 3cc22f82c0..86ef44f2c6 100644 --- a/packages/accounts-password/passwords_client.js +++ b/packages/accounts-password/passwords_client.js @@ -180,6 +180,7 @@ callback && callback(error); } + Accounts._makeClientLoggedIn(result.id, result.token); callback && callback(); }); }; From e0da63b45fc7a3c924bf13cbfd0e0ca991a793d1 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 3 Oct 2012 17:42:22 -0700 Subject: [PATCH 172/239] Minor refactorings and comment tweaks for allow/deny/insecure. --- packages/insecure/package.js | 4 +- packages/mongo-livedata/collection.js | 150 ++++++++++---------------- 2 files changed, 61 insertions(+), 93 deletions(-) diff --git a/packages/insecure/package.js b/packages/insecure/package.js index f468cf80b2..fe2e744a38 100644 --- a/packages/insecure/package.js +++ b/packages/insecure/package.js @@ -1,8 +1,8 @@ Package.describe({ - summary: "Allow all database writes by default", - internal: false + summary: "Allow all database writes by default" }); Package.on_use(function (api) { + api.use(['mongo-livedata']); api.add_files(['insecure.js'], 'server'); }); diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index ca002967f2..78fde407a4 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -250,50 +250,47 @@ _.each(["insert", "update", "remove"], function (name) { // // allow and deny can be called multiple times. The validators are // evaluated as follows: -// - If any deny() function returns true, the request is denied. -// - Otherwise, if any allow() function returns true, the requested is allowed. +// - If neither deny() nor allow() has been called on the collection, +// then the request is allowed if and only if the "insecure" smart +// package is in use. +// - Otherwise, if any deny() function returns true, the request is denied. +// - Otherwise, if any allow() function returns true, the request is allowed. // - Otherwise, the request is denied. +// +// Meteor may call your deny() and allow() functions in any order, and may not +// call all of them if it is able to make a decision without calling them all +// (so don't include side effects). -Meteor.Collection.prototype.allow = function(options) { - var self = this; - self._restricted = true; +(function () { + var addValidator = function(allowOrDeny, options) { + var self = this; + self._restricted = true; - if (options.insert) - self._validators.insert.allow.push(options.insert); - if (options.update) - self._validators.update.allow.push(options.update); - if (options.remove) - self._validators.remove.allow.push(options.remove); + _.each(['insert', 'update', 'remove'], function (name) { + if (options[name]) + self._validators[name][allowOrDeny].push(options[name]); + }); - // Only update the fetch fields if we're passed things that affect - // fetching. This way allow({}) doesn't result in setting - // fetchAllFields - if (options.update || options.remove || options.fetch) - self._updateFetch(options.fetch); -}; - -Meteor.Collection.prototype.deny = function(options) { - var self = this; - self._restricted = true; - - if (options.insert) - self._validators.insert.deny.push(options.insert); - if (options.update) - self._validators.update.deny.push(options.update); - if (options.remove) - self._validators.remove.deny.push(options.remove); - - // same as allow. see above. - if (options.update || options.remove || options.fetch) - self._updateFetch(options.fetch); -}; + // Only update the fetch fields if we're passed things that affect + // fetching. This way allow({}) and allow({insert: f}) don't result in + // setting fetchAllFields + if (options.update || options.remove || options.fetch) + self._updateFetch(options.fetch); + }; + Meteor.Collection.prototype.allow = function(options) { + addValidator.call(this, 'allow', options); + }; + Meteor.Collection.prototype.deny = function(options) { + addValidator.call(this, 'deny', options); + }; +})(); Meteor.Collection.prototype._defineMutationMethods = function() { var self = this; // set to true once we call any allow or deny methods. If true, use - // allow/deny semanitcs. If false, use insecure mode semanitcs. + // allow/deny semantics. If false, use insecure mode semantics. self._restricted = false; // Insecure mode (default to allowing writes). Defaults to 'undefined' @@ -313,54 +310,38 @@ Meteor.Collection.prototype._defineMutationMethods = function() { if (!self._name) return; // anonymous collection - // XXX what if name has illegal characters in it? + // XXX Think about method namespacing. Maybe methods should be + // "Meteor:Mongo:insert/NAME"? self._prefix = '/' + self._name + '/'; // mutation methods if (self._manager) { var m = {}; - // XXX what if name has illegal characters in it? - m[self._prefix + 'insert'] = function (doc) { - self._maybe_snapshot(); - if (this.isSimulation) { - self._collection.insert(doc); - } else if (self._restricted) { - self._validatedInsert(this.userId(), doc); - } else if (self._isInsecure()) { - self._collection.insert(doc); - } else { - throw new Meteor.Error(403, "Access denied"); - } - }; + _.each(['insert', 'update', 'remove'], function (method) { + m[self._prefix + method] = function (/* ... */) { + self._maybe_snapshot(); - m[self._prefix + 'update'] = function (selector, mutator, options) { - self._maybe_snapshot(); + if (this.isSimulation || (!self._restricted && self._isInsecure())) { + self._collection[method].apply( + self._collection, _.toArray(arguments)); + } else if (self._restricted) { + // short circuit if there is no way it will pass. + if (self._validators[method].allow.length === 0) { + throw new Meteor.Error( + 403, "Access denied. No allow validators set on restricted " + + "collection."); + } - if (this.isSimulation) { - self._collection.update(selector, mutator, options); - } else if (self._restricted) { - self._validatedUpdate(this.userId(), selector, mutator, options); - } else if (self._isInsecure()) { - self._collection.update(selector, mutator, options); - } else { - throw new Meteor.Error(403, "Access denied"); - } - }; - - m[self._prefix + 'remove'] = function (selector) { - self._maybe_snapshot(); - - if (this.isSimulation) { - self._collection.remove(selector); - } else if (self._restricted) { - self._validatedRemove(this.userId(), selector); - } else if (self._isInsecure()) { - self._collection.remove(selector); - } else { - throw new Meteor.Error(403, "Access denied"); - } - }; + var validatedMethodName = + '_validated' + method.charAt(0).toUpperCase() + method.slice(1); + var argsWithUserId = [this.userId()].concat(_.toArray(arguments)); + self[validatedMethodName].apply(self, argsWithUserId); + } else { + throw new Meteor.Error(403, "Access denied"); + } + }; + }); self._manager.methods(m); } @@ -391,11 +372,6 @@ Meteor.Collection.prototype._isInsecure = function () { Meteor.Collection.prototype._validatedInsert = function(userId, doc) { var self = this; - // short circuit if there is no way it will pass. - if (self._validators.insert.allow.length === 0) { - throw new Meteor.Error(403, "Access denied. No allow validators set on restricted collection."); - } - // call user validators. // Any deny returns true means denied. if (_.any(self._validators.insert.deny, function(validator) { @@ -417,19 +393,16 @@ Meteor.Collection.prototype._validatedInsert = function(userId, doc) { // control rules set by calls to `allow/deny` are satisfied. If all // pass, rewrite the mongo operation to use $in to set the list of // document ids to change ##ValidatedChange -Meteor.Collection.prototype._validatedUpdate = function(userId, selector, mutator, options) { +Meteor.Collection.prototype._validatedUpdate = function( + userId, selector, mutator, options) { var self = this; - // short circuit. If no allows are set, we know this won't be allowed. - if (self._validators.update.allow.length === 0) { - throw new Meteor.Error(403, "Access denied. No allow validators set on restricted collection."); - } - // compute modified fields var fields = []; _.each(mutator, function (params, op) { if (op[0] !== '$') { - throw new Meteor.Error(403, "Access denied. Can't replace document in restricted collection."); + throw new Meteor.Error( + 403, "Access denied. Can't replace document in restricted collection."); } else { _.each(_.keys(params), function (field) { // treat dotted fields as if they are replacing their @@ -495,11 +468,6 @@ Meteor.Collection.prototype._validatedUpdate = function(userId, selector, mutato Meteor.Collection.prototype._validatedRemove = function(userId, selector) { var self = this; - // short circuit if there is no way it will pass. - if (self._validators.remove.allow.length === 0) { - throw new Meteor.Error(403, "Access denied. No allow validators set on restricted collection."); - } - var findOptions = {}; if (!self._validators.fetchAllFields) { findOptions.fields = {}; From 5e754e93a9f46cee3b203ef99302d4e77d12a605 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 4 Oct 2012 14:36:30 -0700 Subject: [PATCH 173/239] In _validatedUpdate, make sure that the docs we're updating *still* match the original selector. --- packages/mongo-livedata/collection.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 78fde407a4..cb7226d286 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -449,7 +449,10 @@ Meteor.Collection.prototype._validatedUpdate = function( throw new Meteor.Error(403, "Access denied"); } - // construct new $in selector to replace the original one + // Construct new $in selector to augment the original one. This means we'll + // never update any doc we didn't validate. We keep around the original + // selector so that we don't mutate any docs that have been updated to no + // longer match the original selector. var idInClause = {}; idInClause.$in = _.map(docs, function(doc) { return doc._id; @@ -458,7 +461,7 @@ Meteor.Collection.prototype._validatedUpdate = function( self._collection.update.call( self._collection, - idSelector, + {$and: [selector, idSelector]}, mutator, options); }; From f6e15a5dd759b49050bc41e6cd6fac78a6c4a544 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Thu, 4 Oct 2012 19:57:39 -0700 Subject: [PATCH 174/239] Revert "In _validatedUpdate, make sure that the docs we're updating *still* match the" This reverts commit 5e754e93a9f46cee3b203ef99302d4e77d12a605. --- packages/mongo-livedata/collection.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index cb7226d286..78fde407a4 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -449,10 +449,7 @@ Meteor.Collection.prototype._validatedUpdate = function( throw new Meteor.Error(403, "Access denied"); } - // Construct new $in selector to augment the original one. This means we'll - // never update any doc we didn't validate. We keep around the original - // selector so that we don't mutate any docs that have been updated to no - // longer match the original selector. + // construct new $in selector to replace the original one var idInClause = {}; idInClause.$in = _.map(docs, function(doc) { return doc._id; @@ -461,7 +458,7 @@ Meteor.Collection.prototype._validatedUpdate = function( self._collection.update.call( self._collection, - {$and: [selector, idSelector]}, + idSelector, mutator, options); }; From a1c93b60430a93917ccb32635bf14d47cdaf885b Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Thu, 4 Oct 2012 23:51:31 -0700 Subject: [PATCH 175/239] Change URLs to use / instead of ?. It looks much nicer. --- packages/accounts-password/email_tests.js | 6 +++--- packages/accounts-urls/url_client.js | 6 +++--- packages/accounts-urls/url_server.js | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/accounts-password/email_tests.js b/packages/accounts-password/email_tests.js index e657270617..7e4aa8df63 100644 --- a/packages/accounts-password/email_tests.js +++ b/packages/accounts-password/email_tests.js @@ -32,7 +32,7 @@ var match = content.match( new RegExp(window.location.protocol + "//" + - window.location.host + "/#\\?reset-password/(\\S*)")); + window.location.host + "/#\\/reset-password/(\\S*)")); test.isTrue(match); resetPasswordToken = match[1]; })); @@ -71,7 +71,7 @@ var match = content.match( new RegExp(window.location.protocol + "//" + - window.location.host + "/#\\?validate-email/(\\S*)")); + window.location.host + "/#\\/validate-email/(\\S*)")); test.isTrue(match); validateEmailToken = match[1]; })); @@ -150,7 +150,7 @@ var match = content.match( new RegExp(window.location.protocol + "//" + - window.location.host + "/#\\?enroll-account/(\\S*)")); + window.location.host + "/#\\/enroll-account/(\\S*)")); test.isTrue(match); enrollAccountToken = match[1]; })); diff --git a/packages/accounts-urls/url_client.js b/packages/accounts-urls/url_client.js index 2d3f809e4d..be8d22b532 100644 --- a/packages/accounts-urls/url_client.js +++ b/packages/accounts-urls/url_client.js @@ -11,7 +11,7 @@ // strings so that the reset password token is not sent over the wire // on the http request var match; - match = window.location.hash.match(/^\#\?reset-password\/(.*)$/); + match = window.location.hash.match(/^\#\/reset-password\/(.*)$/); if (match) { Accounts._preventAutoLogin = true; Accounts._resetPasswordToken = match[1]; @@ -28,7 +28,7 @@ // would be faster but less DDP-ish (and more specifically, any // non-web DDP app, such as an iOS client, would do something more // in line with the hash fragment approach) - match = window.location.hash.match(/^\#\?validate-email\/(.*)$/); + match = window.location.hash.match(/^\#\/validate-email\/(.*)$/); if (match) { Accounts._preventAutoLogin = true; Accounts._validateEmailToken = match[1]; @@ -38,7 +38,7 @@ // reads an account enrollment token from the url's hash fragment, if // it's there. also don't automatically log the user is, as for // reset password links. - match = window.location.hash.match(/^\#\?enroll-account\/(.*)$/); + match = window.location.hash.match(/^\#\/enroll-account\/(.*)$/); if (match) { Accounts._preventAutoLogin = true; Accounts._enrollAccountToken = match[1]; diff --git a/packages/accounts-urls/url_server.js b/packages/accounts-urls/url_server.js index 55a5cfa2b8..e3443fa2db 100644 --- a/packages/accounts-urls/url_server.js +++ b/packages/accounts-urls/url_server.js @@ -5,13 +5,13 @@ if (!Accounts.urls) Accounts.urls = {}; Accounts.urls.resetPassword = function (token) { - return Meteor.absoluteUrl('#?reset-password/' + token); + return Meteor.absoluteUrl('#/reset-password/' + token); }; Accounts.urls.validateEmail = function (token) { - return Meteor.absoluteUrl('#?validate-email/' + token); + return Meteor.absoluteUrl('#/validate-email/' + token); }; Accounts.urls.enrollAccount = function (token) { - return Meteor.absoluteUrl('#?enroll-account/' + token); + return Meteor.absoluteUrl('#/enroll-account/' + token); }; From a0049073a8fc18a4aa76644b18f2466075a61bbb Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Thu, 4 Oct 2012 23:52:19 -0700 Subject: [PATCH 176/239] accounts-weibo: small clarification in configuration steps --- packages/accounts-weibo/weibo_configure.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/accounts-weibo/weibo_configure.html b/packages/accounts-weibo/weibo_configure.html index a509880207..12837805cc 100644 --- a/packages/accounts-weibo/weibo_configure.html +++ b/packages/accounts-weibo/weibo_configure.html @@ -7,7 +7,7 @@ Visit http://open.weibo.com/development
  • - Click the "创建应用" button + Click the green "创建应用" button
  • Select 网页应用在第三方网页内访问使用 (Web Applications) From 36f55a55b6531577bdfcdd630b7bac798b39fd0c Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 5 Oct 2012 00:51:19 -0700 Subject: [PATCH 177/239] (Take 2.) In _validatedUpdate, make sure that the docs we're updating *still* match the selector. Also, no need to call validators if no docs match the update or remove selector. --- packages/mongo-livedata/allow_tests.js | 22 ++++++++++++++++++---- packages/mongo-livedata/collection.js | 26 +++++++++++++++++++++----- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/packages/mongo-livedata/allow_tests.js b/packages/mongo-livedata/allow_tests.js index feed2b0055..15f92cde81 100644 --- a/packages/mongo-livedata/allow_tests.js +++ b/packages/mongo-livedata/allow_tests.js @@ -314,6 +314,7 @@ (function () { var collection = restrictedCollectionForUpdateOptionsTest; + var id1; testAsyncMulti("collection - update options", [ // init function (test, expect) { @@ -325,20 +326,32 @@ // put a few objects function (test, expect) { var doc = {canInsert: true, canUpdate: true, world: test.runId()}; + id1 = collection.insert(doc); collection.insert(doc); collection.insert(doc); collection.insert(doc, expect(function (err, res) { test.isFalse(err); - test.equal(collection.find({world: test.runId()}).count(), 3); + test.equal(collection.find({world: test.runId()}).count(), 4); })); }, + // update by id + function (test, expect) { + collection.update( + id1, + {$set: {updated: true}}, + expect(function (err, res) { + test.isFalse(err); + test.equal(collection.find({world: test.runId(), updated: true}).count(), 1); + })); + }, // update without the `multi` option function (test, expect) { collection.update( - {world: test.runId()}, + {updated: {$exists: false}, world: test.runId()}, {$set: {updated: true}}, expect(function (err, res) { - test.equal(collection.find({world: test.runId(), updated: true}).count(), 1); + test.isFalse(err); + test.equal(collection.find({world: test.runId(), updated: true}).count(), 2); })); }, // update with the `multi` option @@ -348,7 +361,8 @@ {$set: {updated: true}}, {multi: true}, expect(function (err, res) { - test.equal(collection.find({world: test.runId(), updated: true}).count(), 3); + test.isFalse(err); + test.equal(collection.find({world: test.runId(), updated: true}).count(), 4); })); } ]); diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 78fde407a4..ed22126d52 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -428,6 +428,8 @@ Meteor.Collection.prototype._validatedUpdate = function( var docs; if (options && options.multi) { docs = self._collection.find(selector, findOptions).fetch(); + if (docs.length === 0) // none satisfied! + return; } else { var doc = self._collection.findOne(selector, findOptions); if (!doc) // none satisfied! @@ -449,18 +451,30 @@ Meteor.Collection.prototype._validatedUpdate = function( throw new Meteor.Error(403, "Access denied"); } - // construct new $in selector to replace the original one + // Construct new $in selector to augment the original one. This means we'll + // never update any doc we didn't validate. We keep around the original + // selector so that we don't mutate any docs that have been updated to no + // longer match the original selector. var idInClause = {}; idInClause.$in = _.map(docs, function(doc) { return doc._id; }); var idSelector = {_id: idInClause}; + var fullSelector; + if (LocalCollection._selectorIsId(selector)) { + // If the original selector was just a lookup by _id, no need to "and" it + // with the idSelector (and it won't work anyway without explicitly + // comparing with _id). + if (docs.length !== 1 || docs[0]._id !== selector) + throw new Error("Lookup by ID " + selector + " found something else"); + fullSelector = selector; + } else { + fullSelector = {$and: [selector, idSelector]}; + } + self._collection.update.call( - self._collection, - idSelector, - mutator, - options); + self._collection, fullSelector, mutator, options); }; // Simulate a mongo `remove` operation while validating access control @@ -477,6 +491,8 @@ Meteor.Collection.prototype._validatedRemove = function(userId, selector) { } var docs = self._collection.find(selector, findOptions).fetch(); + if (docs.length === 0) // none satisfied! + return; // call user validators. // Any deny returns true means denied. From a49685a1ec9bf32ae3cadffa832b80b1115d494f Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 5 Oct 2012 01:12:55 -0700 Subject: [PATCH 178/239] Remove "fail" from a test name (to make it easier to find failures). --- packages/livedata/livedata_tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/livedata/livedata_tests.js b/packages/livedata/livedata_tests.js index 73ef886a64..ff1e07b125 100644 --- a/packages/livedata/livedata_tests.js +++ b/packages/livedata/livedata_tests.js @@ -375,7 +375,7 @@ testAsyncMulti("livedata - changing userid reruns subscriptions without flapping } ]); -Tinytest.add("livedata - setUserId fails when called from server", function(test) { +Tinytest.add("livedata - setUserId error when called from server", function(test) { if (Meteor.isServer) { test.equal(errorThrownWhenCallingSetUserIdDirectlyOnServer.message, "Can't call setUserId on a server initiated method call"); From 750629c7181ebf779a1c65b86dd1458442eb9538 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 5 Oct 2012 11:17:12 -0700 Subject: [PATCH 179/239] livedata_connection_tests now create connections which don't prevent reload when they have outstanding methods. --- packages/livedata/livedata_connection.js | 42 ++++++++++++------- .../livedata/livedata_connection_tests.js | 32 +++++++------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index cc5893e08d..58fde708b2 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -12,8 +12,16 @@ Meteor._capture_subs = null; // @param url {String|Object} URL to Meteor app or sockjs endpoint (deprecated), // or an object as a test hook (see code) -Meteor._LivedataConnection = function (url, restart_on_update) { +// Options: +// reloadOnUpdate: should we try to reload when the server says +// there's new code available? +// reloadWithOutstanding: is it OK to reload if there are outstanding methods? +Meteor._LivedataConnection = function (url, options) { var self = this; + options = _.extend({ + reloadOnUpdate: false, + reloadWithOutstanding: false + }, options); // as a test hook, allow passing a stream instead of a url. if (typeof url === "object") { @@ -70,18 +78,19 @@ Meteor._LivedataConnection = function (url, restart_on_update) { // just for testing self.quiesce_callbacks = []; - - // Setup auto-reload persistence. - Meteor._reload.onMigrate(function (retry) { - if (!self._readyToMigrate()) { - if (self._retryMigrate) - throw new Error("Two migrations in progress?"); - self._retryMigrate = retry; - return false; - } else { - return [true]; - } - }); + // Block auto-reload while we're waiting for method responses. + if (!options.reloadWithOutstanding) { + Meteor._reload.onMigrate(function (retry) { + if (!self._readyToMigrate()) { + if (self._retryMigrate) + throw new Error("Two migrations in progress?"); + self._retryMigrate = retry; + return false; + } else { + return [true]; + } + }); + } // Setup stream (if not overriden above) self.stream = self.stream || new Meteor._Stream(self.url); @@ -151,7 +160,7 @@ Meteor._LivedataConnection = function (url, restart_on_update) { }); }); - if (restart_on_update) { + if (options.reloadOnUpdate) { self.stream.on('update_available', function () { // Start trying to migrate to a new version. Until all packages // signal that they're ready for a migration, the app will @@ -775,8 +784,9 @@ _.extend(Meteor, { // "/", // "http://subdomain.meteor.com/sockjs" (deprecated), // "/sockjs" (deprecated) - connect: function (url, _restartOnUpdate) { - var ret = new Meteor._LivedataConnection(url, _restartOnUpdate); + connect: function (url, _reloadOnUpdate) { + var ret = new Meteor._LivedataConnection( + url, {reloadOnUpdate: _reloadOnUpdate}); Meteor._LivedataConnection._allConnections.push(ret); // hack. see below. return ret; }, diff --git a/packages/livedata/livedata_connection_tests.js b/packages/livedata/livedata_connection_tests.js index 0c4a40e8fb..c8ee36ace5 100644 --- a/packages/livedata/livedata_connection_tests.js +++ b/packages/livedata/livedata_connection_tests.js @@ -1,3 +1,10 @@ +var newConnection = function (stream) { + // Some of these tests leave outstanding methods with no result yet + // returned. This should not block us from re-running tests when sources + // change. + return new Meteor._LivedataConnection(stream, {reloadWithOutstanding: true}); +}; + var test_got_message = function (test, stream, expected) { if (stream.sent.length === 0) { test.fail({error: 'no message received', expected: expected}); @@ -26,7 +33,7 @@ var SESSION_ID = '17'; Tinytest.add("livedata stub - receive data", function (test) { var stream = new Meteor._StubStream(); - var conn = new Meteor._LivedataConnection(stream); + var conn = newConnection(stream); startAndConnect(test, stream); @@ -52,7 +59,7 @@ Tinytest.add("livedata stub - receive data", function (test) { Tinytest.add("livedata stub - subscribe", function (test) { var stream = new Meteor._StubStream(); - var conn = new Meteor._LivedataConnection(stream); + var conn = newConnection(stream); startAndConnect(test, stream); @@ -76,7 +83,7 @@ Tinytest.add("livedata stub - subscribe", function (test) { Tinytest.add("livedata stub - this", function (test) { var stream = new Meteor._StubStream(); - var conn = new Meteor._LivedataConnection(stream); + var conn = newConnection(stream); startAndConnect(test, stream); @@ -104,7 +111,7 @@ Tinytest.add("livedata stub - this", function (test) { Tinytest.add("livedata stub - methods", function (test) { var stream = new Meteor._StubStream(); - var conn = new Meteor._LivedataConnection(stream); + var conn = newConnection(stream); startAndConnect(test, stream); @@ -197,7 +204,7 @@ Tinytest.add("livedata stub - methods", function (test) { // method calls another method in simulation. see not sent. Tinytest.add("livedata stub - sub methods", function (test) { var stream = new Meteor._StubStream(); - var conn = new Meteor._LivedataConnection(stream); + var conn = newConnection(stream); startAndConnect(test, stream); @@ -267,7 +274,7 @@ Tinytest.add("livedata stub - sub methods", function (test) { // data is shown Tinytest.add("livedata stub - reconnect", function (test) { var stream = new Meteor._StubStream(); - var conn = new Meteor._LivedataConnection(stream); + var conn = newConnection(stream); startAndConnect(test, stream); @@ -380,7 +387,7 @@ Tinytest.add("livedata stub - reconnect", function (test) { Tinytest.add("livedata connection - reactive userId", function (test) { var stream = new Meteor._StubStream(); - var conn = new Meteor._LivedataConnection(stream); + var conn = newConnection(stream); test.equal(conn.userId(), null); conn.setUserId(1337); @@ -389,7 +396,7 @@ Tinytest.add("livedata connection - reactive userId", function (test) { Tinytest.add("livedata connection - two wait methods with reponse in order", function (test) { var stream = new Meteor._StubStream(); - var conn = new Meteor._LivedataConnection(stream); + var conn = newConnection(stream); startAndConnect(test, stream); // setup method @@ -449,7 +456,7 @@ Tinytest.add("livedata connection - two wait methods with reponse in order", fun Tinytest.add("livedata connection - one wait method with response out of order", function (test) { var stream = new Meteor._StubStream(); - var conn = new Meteor._LivedataConnection(stream); + var conn = newConnection(stream); startAndConnect(test, stream); // setup method @@ -486,14 +493,11 @@ Tinytest.add("livedata connection - one wait method with response out of order", test.equal(three_message.params, ['three!']); // Since we sent it, it should no longer be in "blocked_methods". test.equal(conn.blocked_methods, []); - - stream.receive({msg: 'result', id: three_message.id}); - test.equal(stream.sent.length, 0); }); Tinytest.add("livedata connection - onReconnect prepends messages correctly with a wait method", function(test) { var stream = new Meteor._StubStream(); - var conn = new Meteor._LivedataConnection(stream); + var conn = newConnection(stream); startAndConnect(test, stream); // setup method @@ -533,7 +537,7 @@ Tinytest.add("livedata connection - onReconnect prepends messages correctly with Tinytest.add("livedata connection - onReconnect prepends messages correctly without a wait method", function(test) { var stream = new Meteor._StubStream(); - var conn = new Meteor._LivedataConnection(stream); + var conn = newConnection(stream); startAndConnect(test, stream); // setup method From 1c9d4a7492f5850c2eb0c4d2b58dbad9ee10effb Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Thu, 4 Oct 2012 23:19:20 -0700 Subject: [PATCH 180/239] accounts-ui: support changing password, and some misc reorg - If you're using accounts-password, or you are using more than one oauth provider, show a dropdown instead of the logout button. (This means that loginButtons either always shows a dropdown or never) - If you have a username or email set, show "Change password" and "Logout" in the dropdown. - If you don't have a username or email set, just show "Logout" While at it, refactored some code I touched. --- .../accounts-password/passwords_common.js | 2 +- .../accounts-ui-unstyled/login_buttons.html | 69 +++- .../accounts-ui-unstyled/login_buttons.js | 299 ++++++++++++------ .../login_buttons_images.css | 8 +- packages/accounts-ui/login_buttons.less | 9 +- 5 files changed, 271 insertions(+), 116 deletions(-) diff --git a/packages/accounts-password/passwords_common.js b/packages/accounts-password/passwords_common.js index 69ae38975d..7ad0470e16 100644 --- a/packages/accounts-password/passwords_common.js +++ b/packages/accounts-password/passwords_common.js @@ -1 +1 @@ -Accounts.passwords = {}; +Accounts.password = {}; diff --git a/packages/accounts-ui-unstyled/login_buttons.html b/packages/accounts-ui-unstyled/login_buttons.html index 5538f45028..82f58fbdcd 100644 --- a/packages/accounts-ui-unstyled/login_buttons.html +++ b/packages/accounts-ui-unstyled/login_buttons.html @@ -1,14 +1,11 @@ + + + +