From dd9d213a30508c05c194a3f00dc8bc36267acdaa Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 10 Oct 2012 17:15:14 -0700 Subject: [PATCH 1/6] First draft of Meteor 0.5.0 release notes. --- History.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/History.md b/History.md index 587dbd676a..7201fe49d0 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,49 @@ ## vNEXT +* This release introduces Meteor Accounts, a full-featured auth system that supports + - fine-grained user-based control over database reads and writes + - federated login with Facebook, GitHub, Google, Twitter, and Weibo + - secure password login + - email validation and password recovery + - an optional set of UI widgets implementing standard login/signup/password + change/logout flows + + When you upgrade to Meteor 0.5.0, existing apps will lose the ability to write + to the database from the client. To restore this, either: + - configure each of your collections with + [`collection.allow`](http://docs.meteor.com/#allow) and + [`collection.deny`](http://docs.meteor.com/#deny) calls to specify which + users can perform which write operations, or + - add the `insecure` smart package (which is included in new apps by default) + to restore the old behavior where anyone can write to any collection which + has not been configured with `allow` or `deny` + + For more information on Meteor Accounts, see http://docs.meteor.com/#accounts + +* Arrays and objects can now be stored in the `Session`; mutating the value you + retrieve with `Session.get` does not affect the value in the session. + +* On the client, `Meteor.apply` takes a new `wait` option, which ensures that no + further method calls are sent to the server until this method is finished; it + is used for login and logout methods in order to keep the user ID + well-defined. You can also specifiy an `onReconnect` handler which is run when + re-establishing a connection; Meteor Accounts uses this to log back in on + reconnect. + +* Meteor now provides a compatible replacement for the DOM `localStorage` + facility that works in IE7, in the `localstorage-polyfill` smart package. + +* `Meteor.Collection` now takes its optional `manager` argument (used to + associate a collection with a server you've connected to with + `Meteor.connect`) as a named option. (The old call syntax continues to work + for now.) + +* Fix a bug where trying to immediately resubscribe to a record set after + unsubscribing could fail silently. + +* Better error handling for failed Mongo writes from inside methods; previously, + errors here could cause clients to stop processing data from the server. ## v0.4.2 From 2c6f991228dd1cf39c4829ff6e9a8fdd620d988d Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 10 Oct 2012 17:27:00 -0700 Subject: [PATCH 2/6] Ensure that "meteor list" doesn't truncate any package descriptions. --- packages/amplify/package.js | 2 +- packages/autopublish/package.js | 2 +- packages/force-ssl/package.js | 2 +- packages/preserve-inputs/package.js | 2 +- packages/underscore/package.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/amplify/package.js b/packages/amplify/package.js index b70c41913a..c808eda438 100644 --- a/packages/amplify/package.js +++ b/packages/amplify/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Cross browser API for Persistant Storage, PubSub and Request." + summary: "API for Persistant Storage, PubSub and Request" }); Package.on_use(function (api) { diff --git a/packages/autopublish/package.js b/packages/autopublish/package.js index 7833caae42..ae8c12f22a 100644 --- a/packages/autopublish/package.js +++ b/packages/autopublish/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Automatically publish all data in the database to every client" + summary: "Automatically publish the entire database to all clients" }); Package.on_use(function (api, where) { diff --git a/packages/force-ssl/package.js b/packages/force-ssl/package.js index bc413ad820..e397ec0eaa 100644 --- a/packages/force-ssl/package.js +++ b/packages/force-ssl/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Require this application always use transport layer encryption" + summary: "Require this application to use secure transport (HTTPS)" }); Package.on_use(function (api) { diff --git a/packages/preserve-inputs/package.js b/packages/preserve-inputs/package.js index 3a9d4f0a9d..f8eca26f9b 100644 --- a/packages/preserve-inputs/package.js +++ b/packages/preserve-inputs/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Automatically preserve all form fields that have a unique id" + summary: "Automatically preserve all form fields with a unique id" }); Package.on_use(function (api, where) { diff --git a/packages/underscore/package.js b/packages/underscore/package.js index 71b0a526d7..00b473b891 100644 --- a/packages/underscore/package.js +++ b/packages/underscore/package.js @@ -1,5 +1,5 @@ Package.describe({ - summary: "Collection of small helper functions (map, each, bind, ...)" + summary: "Collection of small helper functions: _.map, _.each, ..." }); Package.on_use(function (api, where) { From 98111eb6c9c5341173ff61dec06c0253179b3288 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 10 Oct 2012 21:41:47 -0700 Subject: [PATCH 3/6] If for some reason your user has no published fields, make sure Meteor.user() still returns a (trivial) object. --- packages/accounts-base/accounts_client.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/accounts-base/accounts_client.js b/packages/accounts-base/accounts_client.js index 0a50eba3d8..796efc1b2f 100644 --- a/packages/accounts-base/accounts_client.js +++ b/packages/accounts-base/accounts_client.js @@ -17,9 +17,13 @@ var userId = Meteor.userId(); if (!userId) return null; - if (Meteor.userLoaded()) - return Meteor.users.findOne(userId); - // Not yet loaded: return a minimal object. + if (Meteor.userLoaded()) { + var user = Meteor.users.findOne(userId); + if (user) return user; + } + // Either the subscription isn't done yet, or for some reason this user has + // no published fields (and thus is considered to not exist in + // minimongo). Return a minimal object. return {_id: userId}; }; From 65872c3c1c5c44927ab1d1463c6ecb73f429778e Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 10 Oct 2012 21:53:08 -0700 Subject: [PATCH 4/6] Test for 98111eb. --- packages/accounts-password/passwords_tests.js | 9 ++++++++- packages/accounts-password/passwords_tests_setup.js | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/accounts-password/passwords_tests.js b/packages/accounts-password/passwords_tests.js index 79058da652..100ac7024d 100644 --- a/packages/accounts-password/passwords_tests.js +++ b/packages/accounts-password/passwords_tests.js @@ -231,7 +231,6 @@ if (Meteor.isClient) (function () { function(test, expect) { test.equal(Meteor.user().profile.touchedByOnCreateUser, true); }, - // test Meteor.user(). This test properly belongs in // accounts-base/accounts_tests.js, but this is where the tests that // actually log in are. @@ -243,6 +242,14 @@ if (Meteor.isClient) (function () { test.equal(err, undefined); })); }, + function(test, expect) { + Meteor.call('clearUsernameAndProfile'); + Meteor.default_connection.onQuiesce(expect(function() { + test.isTrue(Meteor.userId()); + var user = Meteor.user(); + test.equal(user, {_id: Meteor.userId()}); + })); + }, logoutStep, function(test, expect) { var clientUser = Meteor.user(); diff --git a/packages/accounts-password/passwords_tests_setup.js b/packages/accounts-password/passwords_tests_setup.js index bd5aa463e6..10d22aaaa5 100644 --- a/packages/accounts-password/passwords_tests_setup.js +++ b/packages/accounts-password/passwords_tests_setup.js @@ -35,5 +35,11 @@ Accounts.config({ // 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(); } + testMeteorUser: function () { return Meteor.user(); }, + clearUsernameAndProfile: function () { + if (!this.userId) + throw new Error("Not logged in!"); + Meteor.users.update(this.userId, + {$unset: {profile: 1, username: 1}}); + } }); From 00eda434206aba34e91ccde25019c2b035b3c746 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 10 Oct 2012 15:44:58 -0700 Subject: [PATCH 5/6] OAuth login to an existing account now *does* update the services.PROVIDER field (eg, to refresh access tokens) but does *not* update anything else (which is effectively a no-op change because in practice the only thing that it attempted to update was "profile" but because existing fields were not overridden, this never did anything). Remove the "extra" argument from createUser and related functions. Add a new "profile" option to the main options dictionary, interpreted by defaultCreateUserHook. --- packages/accounts-base/accounts_server.js | 68 +++++++++---------- packages/accounts-base/accounts_tests.js | 36 +++++----- packages/accounts-facebook/facebook_server.js | 2 +- packages/accounts-github/github_server.js | 2 +- packages/accounts-google/google_server.js | 2 +- .../accounts-oauth-helper/oauth_server.js | 2 +- .../accounts-oauth1-helper/oauth1_server.js | 2 +- .../accounts-oauth1-helper/oauth1_tests.js | 2 +- .../accounts-oauth2-helper/oauth2_server.js | 2 +- .../accounts-oauth2-helper/oauth2_tests.js | 2 +- packages/accounts-password/email_tests.js | 6 +- .../accounts-password/passwords_client.js | 9 +-- .../accounts-password/passwords_server.js | 17 ++--- packages/accounts-password/passwords_tests.js | 22 +++--- .../passwords_tests_setup.js | 6 +- packages/accounts-twitter/twitter_server.js | 2 +- packages/accounts-weibo/weibo_server.js | 2 +- 17 files changed, 84 insertions(+), 100 deletions(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 9a4b112466..10d77fa56f 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -115,21 +115,13 @@ }; // XXX see comment on Accounts.createUser in passwords_server about adding a - // third "server options" argument. - var defaultCreateUserHook = function (options, extra, user) { - // This hook gets 'extra' directly from the createUser method, so make sure - // we don't allow users to set any fields at creation time that they won't - // later be able to set according to the default Meteor.users.allow. Set - // your own onCreateUser if you want users to be able to specify other - // fields at creation time. - if (_.any(extra, function(value, key) {return key != 'profile';})) { - console.log(JSON.stringify(extra)); - throw new Meteor.Error(400, "Disallowed fields in extra"); - } - - return _.extend(user, extra); + // second "server options" argument. + var defaultCreateUserHook = function (options, user) { + if (options.profile) + user.profile = options.profile; + return user; }; - Accounts.insertUserDoc = function (options, extra, user) { + Accounts.insertUserDoc = function (options, user) { // add created at timestamp (and protect passed in user object from // modification) user = _.extend({createdAt: +(new Date)}, user); @@ -137,15 +129,15 @@ var fullUser; if (onCreateUserHook) { - fullUser = onCreateUserHook(options, extra, user); + fullUser = onCreateUserHook(options, user); // This is *not* part of the API. We need this because we can't isolate // the global server environment between tests, meaning we can't test // both having a create user hook set and not having one set. if (fullUser === 'TEST DEFAULT HOOK') - fullUser = defaultCreateUserHook(options, extra, user); + fullUser = defaultCreateUserHook(options, user); } else { - fullUser = defaultCreateUserHook(options, extra, user); + fullUser = defaultCreateUserHook(options, user); } _.each(validateNewUserHooks, function (hook) { @@ -199,13 +191,13 @@ // @param serviceData {Object} Data to store in the user's record // under services[serviceName]. Must include an "id" field // which is a unique identifier for the user in the service. - // @param extra {Object, optional} Any additional fields to place on the user - // object + // @param options {Object, optional} Other options to pass to insertUserDoc + // (eg, profile) // @returns {Object} Object with token and id keys, like the result // of the "login" method. Accounts.updateOrCreateUserFromExternalService = function( - serviceName, serviceData, extra) { - extra = extra || {}; + serviceName, serviceData, options) { + options = _.clone(options || {}); if (serviceName === "password" || serviceName === "resume") throw new Error( @@ -221,26 +213,28 @@ var user = Meteor.users.findOne(selector); if (user) { - // don't overwrite existing fields - // XXX subobjects (aka 'profile', 'services')? - var newKeys = _.difference(_.keys(extra), _.keys(user)); - var newAttrs = _.pick(extra, newKeys); + // We *don't* process options (eg, profile) for update, but we do replace + // the serviceData (eg, so that we keep an unexpired access token and + // don't cache old email addresses in serviceData.email). + // XXX provide an onUpdateUser hook which would let apps update + // the profile too var stampedToken = Accounts._generateStampedLoginToken(); - var result = {token: stampedToken.token}; + var setAttrs = {}; + setAttrs["services." + serviceName] = serviceData; + // XXX Maybe we should re-use the selector above and notice if the update + // touches nothing? Meteor.users.update( user._id, - {$set: newAttrs, $push: {'services.resume.loginTokens': stampedToken}}); - result.id = user._id; - return result; + {$set: setAttrs, + $push: {'services.resume.loginTokens': stampedToken}}); + return {token: stampedToken.token, id: user._id}; } else { - // Create a new user. - var servicesClause = {}; - servicesClause[serviceName] = serviceData; - var insertOptions = {services: servicesClause, generateLoginToken: true}; - // Build a user doc; clone to make sure sure mutating - // insertOptions.services doesn't affect user.services or vice versa. - user = {services: JSON.parse(JSON.stringify(servicesClause))}; - return Accounts.insertUserDoc(insertOptions, extra, user); + // Create a new user with the service data. Pass other options through to + // insertUserDoc. + user = {services: {}}; + user.services[serviceName] = serviceData; + options.generateLoginToken = true; + return Accounts.insertUserDoc(options, user); } }; diff --git a/packages/accounts-base/accounts_tests.js b/packages/accounts-base/accounts_tests.js index d350d811d6..b771e17de1 100644 --- a/packages/accounts-base/accounts_tests.js +++ b/packages/accounts-base/accounts_tests.js @@ -6,20 +6,24 @@ Tinytest.add('accounts - updateOrCreateUserFromExternalService', function (test) // create an account with facebook var uid1 = Accounts.updateOrCreateUserFromExternalService( - 'facebook', {id: facebookId}, {profile: {foo: 1}}).id; - test.equal(Meteor.users.find({"services.facebook.id": facebookId}).count(), 1); - test.equal(Meteor.users.findOne({"services.facebook.id": facebookId}).profile.foo, 1); + 'facebook', {id: facebookId, monkey: 42}, {profile: {foo: 1}}).id; + var users = Meteor.users.find({"services.facebook.id": facebookId}).fetch(); + test.length(users, 1); + test.equal(users[0].profile.foo, 1); + test.equal(users[0].services.facebook.monkey, 42); - // create again with the same id, see that we get the same user. profile - // doesn't get overwritten in this implementation (though we should do - // something better with merging later). + // create again with the same id, see that we get the same user. + // it should update services.facebook but not profile. var uid2 = Accounts.updateOrCreateUserFromExternalService( - 'facebook', {id: facebookId}, {profile: {foo: 1000, bar: 2}}).id; + 'facebook', {id: facebookId, llama: 50}, + {profile: {foo: 1000, bar: 2}}).id; test.equal(uid1, uid2); - test.equal(Meteor.users.find({"services.facebook.id": facebookId}).count(), 1); - test.equal(Meteor.users.findOne(uid1).profile.foo, 1); - test.equal(Meteor.users.findOne(uid1).profile.bar, undefined); - + users = Meteor.users.find({"services.facebook.id": facebookId}).fetch(); + test.length(users, 1); + test.equal(users[0].profile.foo, 1); + test.equal(users[0].profile.bar, undefined); + test.equal(users[0].services.facebook.llama, 50); + test.equal(users[0].services.facebook.monkey, undefined); // cleanup Meteor.users.remove(uid1); @@ -48,7 +52,6 @@ Tinytest.add('accounts - insertUserDoc username', function (test) { // user does not already exist. create a user object with fields set. var result = Accounts.insertUserDoc( - userIn, {profile: {name: 'Foo Bar'}}, userIn ); @@ -61,7 +64,6 @@ Tinytest.add('accounts - insertUserDoc username', function (test) { // run the hook again. now the user exists, so it throws an error. test.throws(function () { Accounts.insertUserDoc( - userIn, {profile: {name: 'Foo Bar'}}, userIn ); @@ -83,7 +85,6 @@ Tinytest.add('accounts - insertUserDoc email', function (test) { // user does not already exist. create a user object with fields set. var result = Accounts.insertUserDoc( - userIn, {profile: {name: 'Foo Bar'}}, userIn ); @@ -97,7 +98,6 @@ Tinytest.add('accounts - insertUserDoc email', function (test) { // run the hook again. now the user exists, so it throws an error. test.throws(function () { Accounts.insertUserDoc( - userIn, {profile: {name: 'Foo Bar'}}, userIn ); @@ -106,20 +106,20 @@ Tinytest.add('accounts - insertUserDoc email', function (test) { // now with only one of them. test.throws(function () { Accounts.insertUserDoc( - {}, {}, {emails: [{address: email1}]} + {}, {emails: [{address: email1}]} ); }); test.throws(function () { Accounts.insertUserDoc( - {}, {}, {emails: [{address: email2}]} + {}, {emails: [{address: email2}]} ); }); // a third email works. var result3 = Accounts.insertUserDoc( - {}, {}, {emails: [{address: email3}]} + {}, {emails: [{address: email3}]} ); var user3 = Meteor.users.findOne(result3.id); test.equal(typeof user3.createdAt, 'number'); diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index 2184bb1f36..2a003f9003 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -11,7 +11,7 @@ accessToken: accessToken, email: identity.email }, - extra: {profile: {name: identity.name}} + options: {profile: {name: identity.name}} }; }); diff --git a/packages/accounts-github/github_server.js b/packages/accounts-github/github_server.js index ae695e3230..f9dc9147fb 100644 --- a/packages/accounts-github/github_server.js +++ b/packages/accounts-github/github_server.js @@ -11,7 +11,7 @@ email: identity.email, username: identity.login }, - extra: {profile: {name: identity.name}} + options: {profile: {name: identity.name}} }; }); diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index d0e8726c71..167e5f1b75 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -11,7 +11,7 @@ accessToken: accessToken, email: identity.email }, - extra: {profile: {name: identity.name}} + options: {profile: {name: identity.name}} }; }); diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index 5e49755a54..c94e540a42 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -14,7 +14,7 @@ // - (For OAuth1 only) oauthBinding {OAuth1Binding} bound to the appropriate provider // - (For OAuth2 only) query {Object} parameters passed in query string // - return value is: - // - {serviceData, (optional extra)} where serviceData should end + // - {serviceData:, (optional options:)} where serviceData should end // up in the user's services[name] field // - `null` if the user declined to give permissions Accounts.oauth.registerService = function (name, version, handleOauthRequest) { diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 9733417d73..cd2e934871 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -55,7 +55,7 @@ // Get or create user doc and login token for reconnect. Accounts.oauth._loginResultForState[query.state] = Accounts.updateOrCreateUserFromExternalService( - service.serviceName, oauthResult.serviceData, oauthResult.extra); + service.serviceName, oauthResult.serviceData, oauthResult.options); } } diff --git a/packages/accounts-oauth1-helper/oauth1_tests.js b/packages/accounts-oauth1-helper/oauth1_tests.js index b6610e892a..78c0318088 100644 --- a/packages/accounts-oauth1-helper/oauth1_tests.js +++ b/packages/accounts-oauth1-helper/oauth1_tests.js @@ -92,7 +92,7 @@ Tinytest.add("oauth1 - error in user creation", function (test) { accessToken: twitterfailAccessToken, accessTokenSecret: twitterfailAccessTokenSecret }, - extra: { + options: { profile: {invalid: true} } }; diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index f0104418ed..696a59afe5 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -14,7 +14,7 @@ // Get or create user doc and login token for reconnect. Accounts.oauth._loginResultForState[query.state] = Accounts.updateOrCreateUserFromExternalService( - service.serviceName, oauthResult.serviceData, oauthResult.extra); + service.serviceName, oauthResult.serviceData, oauthResult.options); } // Either close the window, redirect, or render nothing diff --git a/packages/accounts-oauth2-helper/oauth2_tests.js b/packages/accounts-oauth2-helper/oauth2_tests.js index b10b183c9f..2331f691fb 100644 --- a/packages/accounts-oauth2-helper/oauth2_tests.js +++ b/packages/accounts-oauth2-helper/oauth2_tests.js @@ -55,7 +55,7 @@ Tinytest.add("oauth2 - error in user creation", function (test) { serviceData: { id: failbookId }, - extra: { + options: { profile: {invalid: true} } }; diff --git a/packages/accounts-password/email_tests.js b/packages/accounts-password/email_tests.js index 843e98c261..fc7da6b80e 100644 --- a/packages/accounts-password/email_tests.js +++ b/packages/accounts-password/email_tests.js @@ -17,9 +17,9 @@ function (test, expect) { email1 = Meteor.uuid() + "-intercept@example.com"; Accounts.createUser({email: email1, password: 'foobar'}, - expect(function (error) { - test.equal(error, undefined); - })); + expect(function (error) { + test.equal(error, undefined); + })); }, function (test, expect) { Accounts.forgotPassword({email: email1}, expect(function (error) { diff --git a/packages/accounts-password/passwords_client.js b/packages/accounts-password/passwords_client.js index 00e21e0498..2b877c36ea 100644 --- a/packages/accounts-password/passwords_client.js +++ b/packages/accounts-password/passwords_client.js @@ -1,12 +1,7 @@ (function () { - Accounts.createUser = function (options, extra, callback) { + Accounts.createUser = function (options, callback) { options = _.clone(options); // we'll be modifying options - if (typeof extra === "function") { - callback = extra; - extra = {}; - } - if (!options.password) throw new Error("Must set options.password"); var verifier = Meteor._srp.generateVerifier(options.password); @@ -14,7 +9,7 @@ delete options.password; options.srp = verifier; - Meteor.apply('createUser', [options, extra], {wait: true}, + Meteor.apply('createUser', [options], {wait: true}, function (error, result) { if (error || !result) { error = error || new Error("No result"); diff --git a/packages/accounts-password/passwords_server.js b/packages/accounts-password/passwords_server.js index 5ce4b5bf06..7fb474a773 100644 --- a/packages/accounts-password/passwords_server.js +++ b/packages/accounts-password/passwords_server.js @@ -355,8 +355,7 @@ // // returns an object with id: userId, and (if options.generateLoginToken is // set) token: loginToken. - var createUser = function (options, extra) { - extra = extra || {}; + var createUser = function (options) { var username = options.username; var email = options.email; if (!username && !email) @@ -379,19 +378,19 @@ if (email) user.emails = [{address: email, verified: false}]; - return Accounts.insertUserDoc(options, extra, user); + return Accounts.insertUserDoc(options, user); }; // method for create user. Requests come from the client. Meteor.methods({ - createUser: function (options, extra) { + createUser: function (options) { options = _.clone(options); options.generateLoginToken = true; if (Accounts._options.forbidClientAccountCreation) throw new Meteor.Error(403, "Signups forbidden"); // Create user. result contains id and token. - var result = createUser(options, extra); + var result = createUser(options); // safety belt. createUser is supposed to throw on error. send 500 error // instead of sending a verification email with empty userid. if (!result.id) @@ -420,20 +419,16 @@ // which is always empty when called from the createUser method? eg, "admin: // true", which we want to prevent the client from setting, but which a custom // method calling Accounts.createUser could set? - Accounts.createUser = function (options, extra, callback) { + Accounts.createUser = function (options, callback) { options = _.clone(options); options.generateLoginToken = false; - if (typeof extra === "function") { - callback = extra; - extra = {}; - } // XXX allow an optional callback? if (callback) { throw new Error("Meteor.createUser with callback not supported on the server yet."); } - var userId = createUser(options, extra).id; + var userId = createUser(options).id; return userId; }; diff --git a/packages/accounts-password/passwords_tests.js b/packages/accounts-password/passwords_tests.js index 100ac7024d..99177c2474 100644 --- a/packages/accounts-password/passwords_tests.js +++ b/packages/accounts-password/passwords_tests.js @@ -199,9 +199,9 @@ if (Meteor.isClient) (function () { logoutStep, // test Accounts.validateNewUser function(test, expect) { - Accounts.createUser({username: username3, password: password3}, - // should fail the new user validators - {profile: {invalid: true}}, + Accounts.createUser({username: username3, password: password3, + // should fail the new user validators + profile: {invalid: true}}, expect(function (error) { test.equal(error.error, 403); test.equal( @@ -211,10 +211,10 @@ if (Meteor.isClient) (function () { }, logoutStep, function(test, expect) { - Accounts.createUser({username: username3, password: password3}, + Accounts.createUser({username: username3, password: password3, // should fail the new user validator with a special // exception - {profile: {invalidAndThrowException: true}}, + profile: {invalidAndThrowException: true}}, expect(function (error) { test.equal( error.reason, @@ -224,8 +224,8 @@ if (Meteor.isClient) (function () { // test Accounts.onCreateUser function(test, expect) { Accounts.createUser( - {username: username3, password: password3}, - {testOnCreateUserHook: true}, + {username: username3, password: password3, + testOnCreateUserHook: true}, loggedInAs(username3, test, expect)); }, function(test, expect) { @@ -282,14 +282,14 @@ if (Meteor.isServer) (function () { var email = Meteor.uuid() + '@example.com'; test.throws(function () { // should fail the new user validators - Accounts.createUser({email: email}, {profile: {invalid: true}}); + Accounts.createUser({email: email, profile: {invalid: true}}); }); // disable sending emails var oldEmailSend = Email.send; Email.send = function() {}; - var userId = Accounts.createUser({email: email}, - {testOnCreateUserHook: true}); + var userId = Accounts.createUser({email: email, + testOnCreateUserHook: true}); Email.send = oldEmailSend; test.isTrue(userId); @@ -303,7 +303,7 @@ if (Meteor.isServer) (function () { function (test) { var username = Meteor.uuid(); - var userId = Accounts.createUser({username: username}, {}); + var userId = Accounts.createUser({username: username}); var user = Meteor.users.findOne(userId); // no services yet. diff --git a/packages/accounts-password/passwords_tests_setup.js b/packages/accounts-password/passwords_tests_setup.js index 10d22aaaa5..e1142de742 100644 --- a/packages/accounts-password/passwords_tests_setup.js +++ b/packages/accounts-password/passwords_tests_setup.js @@ -4,9 +4,9 @@ Accounts.validateNewUser(function (user) { return !(user.profile && user.profile.invalid); }); -Accounts.onCreateUser(function (options, extra, user) { - if (extra.testOnCreateUserHook) { - user.profile = (user.profile || {}); +Accounts.onCreateUser(function (options, user) { + if (options.testOnCreateUserHook) { + user.profile = user.profile || {}; user.profile.touchedByOnCreateUser = true; return user; } else { diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 5414dae9d9..4008224b3d 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -10,7 +10,7 @@ accessToken: oauthBinding.accessToken, accessTokenSecret: oauthBinding.accessTokenSecret }, - extra: { + options: { profile: { name: identity.name } diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index fe5e4bb4e6..a061d5054d 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -11,7 +11,7 @@ accessToken: accessToken.access_token, screenName: identity.screen_name }, - extra: {profile: {name: identity.screen_name}} + options: {profile: {name: identity.screen_name}} }; }); From d5ccedf8c12dd285d44292a20dfeaff45d5b75a1 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Wed, 10 Oct 2012 22:16:07 -0700 Subject: [PATCH 6/6] harness for viewing states of accounts-ui --- .../accounts-ui-viewer/.meteor/.gitignore | 1 + .../accounts-ui-viewer/.meteor/packages | 15 ++ .../accounts-ui-viewer.html | 112 +++++++++ .../accounts-ui-viewer/accounts-ui-viewer.js | 215 ++++++++++++++++++ .../accounts-ui-viewer.less | 110 +++++++++ 5 files changed, 453 insertions(+) create mode 100644 examples/unfinished/accounts-ui-viewer/.meteor/.gitignore create mode 100644 examples/unfinished/accounts-ui-viewer/.meteor/packages create mode 100644 examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.html create mode 100644 examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js create mode 100644 examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.less diff --git a/examples/unfinished/accounts-ui-viewer/.meteor/.gitignore b/examples/unfinished/accounts-ui-viewer/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/examples/unfinished/accounts-ui-viewer/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/examples/unfinished/accounts-ui-viewer/.meteor/packages b/examples/unfinished/accounts-ui-viewer/.meteor/packages new file mode 100644 index 0000000000..634f653e55 --- /dev/null +++ b/examples/unfinished/accounts-ui-viewer/.meteor/packages @@ -0,0 +1,15 @@ +# Meteor packages used by this project, one per line. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +autopublish +insecure +preserve-inputs +accounts-ui +less +accounts-google +accounts-github +accounts-password +underscore +accounts-facebook diff --git a/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.html b/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.html new file mode 100644 index 0000000000..9189a5972e --- /dev/null +++ b/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.html @@ -0,0 +1,112 @@ + + accounts-ui-viewer + + + + {{> page}} + + + + + + + diff --git a/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js b/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js new file mode 100644 index 0000000000..98ec13ea2c --- /dev/null +++ b/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.js @@ -0,0 +1,215 @@ +Meteor.users.allow({update: function () { return true; }}); + +if (Meteor.isClient) { + + Accounts.STASH = _.extend({}, Accounts); + + var handleSetting = function (key, value) { + if (key === "numServices") { + _.each(['facebook', 'github', 'google'], + function (serv, i) { + if (i < value) + Accounts[serv] = Accounts.STASH[serv]; + else + Accounts[serv] = null; + }); + } else if (key === "hasPasswords") { + Accounts.password = value && Accounts.STASH.password || null; + var user = Meteor.user(); + if (user) { + if (! value) { + // make sure we have no username if "app" has no passwords + Meteor.users.update(Meteor.userId(), + { $unset: { username: 1 }}); + } else { + // make sure we have a username + Meteor.users.update(Meteor.userId(), + { $set: { username: Meteor.uuid() }}); + } + } + } else if (key === "signupFields") { + Accounts.ui._options.passwordSignupFields = value; + } + }; + + if (! Session.get('settings')) + Session.set('settings', { + openLeft: false, + positioning: "relative", + numServices: 3, + hasPasswords: true, + signupFields: 'EMAIL_ONLY' + }); + else + _.each(Session.get('settings'), function (v,k) { + handleSetting(k, v); + }); + + Template.page.settings = function () { + return Session.get('settings'); + }; + + Template.page.settingsClass = function () { + var settings = Session.get('settings'); + var classes = []; + if (settings.positioning) + classes.push('positioning-' + settings.positioning.toLowerCase()); + return classes.join(' '); + }; + + Template.page.outerClass = function () { + var settings = Session.get('settings'); + var classes = []; + if (settings.openLeft) + classes.push('login-buttons-dropdown-hangs-left'); + return classes.join(' '); + }; + + var keyValueFromId = function (id) { + var match; + if (id && (match = /^(.*?):(.*)$/.exec(id))) { + var key = match[1]; + var value = castValue(match[2]); + return [key, value]; + } + return null; + }; + + var castValue = function (value) { + if (value === "false") + value = false; + else if (value === "true") + value = true; + else if (/^[0-9]+$/.test(value)) + value = Number(value); + return value; + }; + + Template.radio.maybeChecked = function () { + var curValue = Session.get('settings')[this.key]; + if (castValue(this.value) === curValue) + return 'checked="checked"'; + return ''; + }; + + Template.page.radio = function (key, value, label) { + return new Handlebars.SafeString( + Template.radio({key: key, value: value, label: label})); + }; + + Template.page.button = function (key, value, label) { + return new Handlebars.SafeString( + Template.button({key: key, value: value, label: label})); + }; + + Template.page.match = function (kv) { + kv = keyValueFromId(kv); + if (! kv) + return false; + + return Session.get('settings')[kv[0]] === kv[1]; + }; + + var fakeLogin = function () { + Accounts.createUser( + {username: Meteor.uuid(), + password: "password", + profile: { name: "Joe Schmoe" }}, + function () { + var user = Meteor.user(); + if (! user) + return; + // delete our username if we are in a mode + // where there aren't usernames/emails/passwords + // (only third-party auth) so that there is no + // "Change Password" button when signed in + if (! Session.get('settings').hasPasswords) + Meteor.users.update(Meteor.userId(), + { $unset: { username: 1 }}); + }); + }; + + var exitFlows = function () { + Accounts._loginButtonsSession.set('inSignupFlow', false); + Accounts._loginButtonsSession.set('inForgotPasswordFlow', false); + Accounts._loginButtonsSession.set('inChangePasswordFlow', false); + Accounts._loginButtonsSession.set('inMessageOnlyFlow', false); + }; + + Template.page.events({ + 'change #controlpane input[type=radio]': function (event) { + var input = event.currentTarget; + var keyValue; + if (input && input.id && (keyValue = keyValueFromId(input.id))) { + var key = keyValue[0]; + var value = keyValue[1]; + if (value === "false") + value = false; + else if (value === "true") + value = true; + var settings = Session.get('settings'); + settings[key] = value; + Session.set('settings', settings); + + handleSetting(key, value); + } + }, + 'click #controlpane button': function (event) { + if (this.key === "fakeConfig") { + var service = this.value; + if (! Accounts.loginServiceConfiguration.findOne({service: service})) + Accounts.loginServiceConfiguration.insert( + {service: service, fake: true}); + } else if (this.key === "unconfig") { + var service = this.value; + Accounts.loginServiceConfiguration.remove({service: service}); + } else if (this.key === "messages") { + if (this.value === "error") { + Accounts._loginButtonsSession.set('errorMessage', 'An error occurred! Gee golly gosh.'); + } else if (this.value === "info") { + Accounts._loginButtonsSession.set('infoMessage', 'Here is some information that is crucial.'); + } else if (this.value === "clear") { + Accounts._loginButtonsSession.resetMessages(); + } + } else if (this.key === "sign") { + if (this.value === 'in') { + // create a random new user + Accounts._loginButtonsSession.closeDropdown(); + fakeLogin(); + } else if (this.value === 'out') { + Meteor.logout(); + } + } else if (this.key === "showConfig") { + Accounts._loginButtonsSession.configureService(this.value); + } else if (this.key === "lov") { + exitFlows(); + Accounts._loginButtonsSession.set("dropdownVisible", true); + if (Meteor.userId()) + Meteor.logout(); + if (this.value === "createAccount") + Accounts._loginButtonsSession.set("inSignupFlow", true); + else if (this.value === "forgotPassword") + Accounts._loginButtonsSession.set("inForgotPasswordFlow", true); + } else if (this.key === "liv") { + exitFlows(); + Accounts._loginButtonsSession.set("dropdownVisible", true); + if (! Meteor.userId()) + fakeLogin(); + if (this.value === "changePassword") + Accounts._loginButtonsSession.set("inChangePasswordFlow", true); + else if (this.value === "messageOnly") + Accounts._loginButtonsSession.set("inMessageOnlyFlow", true); + } else if (this.key === "modals") { + var value = this.value; + _.each([ + 'resetPasswordToken', + 'enrollAccountToken', + 'justVerifiedEmail'], function (k) { + Accounts._loginButtonsSession.set( + k, k.indexOf(value) >= 0 ? 'foo' : null); + }); + } + } + }); + +} \ No newline at end of file diff --git a/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.less b/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.less new file mode 100644 index 0000000000..1e5a017cc7 --- /dev/null +++ b/examples/unfinished/accounts-ui-viewer/accounts-ui-viewer.less @@ -0,0 +1,110 @@ + +* { padding: 0; margin: 0; } +html, body { height: 100%; } + +#controlpane { + position: absolute; + left: 0; + width: 299px; + top: 0; + bottom: 0; + + background: #eee; + border-right: 1px solid #999; + + overflow: auto; + + h3 { + border-top: 1px solid #999; + font-size: 85%; + margin-bottom: 5px; + } + + .group { + margin: 10px; + } + + input[type=radio] { + margin-left: 5px; + vertical-align: middle; + } + + label { + padding-left: 3px; + } +} + +#previewpane { + position: absolute; + left: 300px; + right: 0; + top: 0; + bottom: 0; + + #preview-wrapper { + margin: 20px; + } +} + +.radio { + white-space: nowrap; +} + +.positioning-floatright { + #login-buttons { + float: right; + margin-right: 180px; + } + + #pos-indicator { + display: block; + top: 0; + right: 0; + width: 200px; + height: 20px; + } +} + +.positioning-relative { + #login-buttons { + position: relative; + left: 120px; + top: 20px; + } + + #pos-indicator { + display: block; + top: 0; + left: 0; + width: 140px; + height: 40px; + } +} + +.positioning-absolute { + #login-buttons { + position: absolute; + left: 140px; + top: 40px; + } + + #pos-indicator { + display: block; + top: 0; + left: 0; + width: 140px; + height: 40px; + } +} + +#pos-indicator { + position: absolute; + background: #eec; + display: none; +} + +a { color: blue; } + +button { padding: 4px; + margin-bottom: 4px; // for when buttons wrap + } \ No newline at end of file