diff --git a/packages/accounts-passwords/package.js b/packages/accounts-passwords/package.js index c84d6cb5b1..afe71c001d 100644 --- a/packages/accounts-passwords/package.js +++ b/packages/accounts-passwords/package.js @@ -14,7 +14,7 @@ Package.on_use(function(api) { Package.on_test(function(api) { api.use(['accounts-passwords', 'tinytest', 'test-helpers']); api.add_files('passwords_tests_setup.js', 'server'); - api.add_files('passwords_tests.js', 'client'); + api.add_files('passwords_tests.js', ['client', 'server']); api.add_files('email_tests_setup.js', 'server'); api.add_files('email_tests.js', 'client'); }); diff --git a/packages/accounts-passwords/passwords_client.js b/packages/accounts-passwords/passwords_client.js index 2edec54502..9919f6e784 100644 --- a/packages/accounts-passwords/passwords_client.js +++ b/packages/accounts-passwords/passwords_client.js @@ -1,5 +1,7 @@ (function () { Meteor.createUser = function (options, extra, callback) { + options = _.clone(options); // we'll be modifying options + if (typeof extra === "function") { callback = extra; extra = {}; @@ -8,15 +10,14 @@ if (!options.password) throw new Error("Must set options.password"); var verifier = Meteor._srp.generateVerifier(options.password); + // strip old password, replacing with the verifier object + delete options.password; + options.srp = verifier; if (options.validation) // needed because we generate a link back to the app options.baseUrl = appBaseUrl(); - // strip old password, replacing with the verifier object - delete options.password; - options.srp = verifier; - Meteor.apply('createUser', [options, extra], {wait: true}, function (error, result) { if (error || !result) { diff --git a/packages/accounts-passwords/passwords_server.js b/packages/accounts-passwords/passwords_server.js index 4510685893..f66c1de2d0 100644 --- a/packages/accounts-passwords/passwords_server.js +++ b/packages/accounts-passwords/passwords_server.js @@ -104,49 +104,6 @@ return ret; }, - createUser: function (options, extra) { - extra = extra || {}; - var username = options.username; - var email = options.email; - if (!username && !email) - throw new Meteor.Error(400, "Need to set a username or email"); - if (options.validation && !options.baseUrl) - throw new Meteor.Error( - 400, "If options.validation is set, need to pass options.baseUrl"); - if (username && Meteor.users.findOne({username: username})) - throw new Meteor.Error(403, "User already exists with username " + username); - if (email && Meteor.users.findOne({"emails.email": email})) { - throw new Meteor.Error(403, "User already exists with email " + email); - } - - // XXX validate verifier - - // raw password, should only be used over SSL! - if (options.password) { - if (options.srp) - throw new Meteor.Error(400, "Don't pass both password and srp in options"); - options.srp = Meteor._srp.generateVerifier(options.password); - } - - var user = {services: {password: {srp: options.srp}}}; - if (username) - user.username = username; - if (email) - user.emails = [{email: email, validated: false}]; - - user = Meteor.accounts.onCreateUserHook(options, extra, user); - var userId = Meteor.users.insert(user); - - // If `options.validation` is set, register a token to validate - // the user's primary email, and send it to that address. - if (email && options.validation) - Meteor.accounts.sendValidationEmail(userId, email, options.baseUrl); - - var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); - this.setUserId(userId); - return {token: loginToken, id: userId}; - }, - forgotPassword: function (options) { var email = options.email; var baseUrl = options.baseUrl; @@ -290,6 +247,125 @@ return {token: loginToken, id: user._id}; }); + + + + //////////// + // Creating users: + + + // Shared createUser function called from the createUser method, both + // if originates in client or server code. Calls user provided hooks, + // does the actual user insertion. + // + // returns userId or throws an error if it can't create + var createUser = function (options, extra) { + extra = extra || {}; + var username = options.username; + var email = options.email; + if (!username && !email) + throw new Meteor.Error(400, "Need to set a username or email"); + + // XXX need to get base url on the server somehow, for welcome + // emails at least. + if (options.validation && !options.baseUrl) + throw new Meteor.Error( + 400, "If options.validation is set, need to pass options.baseUrl"); + + if (username && Meteor.users.findOne({username: username})) + throw new Meteor.Error(403, "User already exists with username " + username); + if (email && Meteor.users.findOne({"emails.email": email})) { + throw new Meteor.Error(403, "User already exists with email " + email); + } + + // Raw password. The meteor client doesn't send this, but a DDP + // client that didn't implement SRP could send this. This should + // only be done over SSL. + if (options.password) { + if (options.srp) + throw new Meteor.Error(400, "Don't pass both password and srp in options"); + options.srp = Meteor._srp.generateVerifier(options.password); + } + + var user = {services: {}}; + if (options.srp) + user.services.password = {srp: options.srp}; // XXX validate verifier + if (username) + user.username = username; + if (email) + user.emails = [{email: email, validated: false}]; + + user = Meteor.accounts.onCreateUserHook(options, extra, user); + var userId = Meteor.users.insert(user); + + // If `options.validation` is set, register a token to validate + // the user's primary email, and send it to that address. + if (email && options.validation) + Meteor.accounts.sendValidationEmail(userId, email, options.baseUrl); + + // XXX send welcome email. + // + // Is a welcome email just a validation email with a password prompt + // as well? + + + return userId; + }; + + // method for create user. Requests come from the client. + Meteor.methods({ + createUser: function (options, extra) { + if (Meteor.accounts._options.forbidSignups) + throw new Meteor.Error(403, "Signups forbidden"); + + var userId = createUser(options, extra); + // safety belt. createUser is supposed to throw on error. send 500 + // error instead of creating a login token with empty userid. + if (!userId) + throw new Error("createUser failed to insert new user"); + + // client gets logged in as the new user afterwards. + var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); + this.setUserId(userId); + return {token: loginToken, id: userId}; + } + }); + + // Create user directly on the server. + // + // Unlike the client version, this does not log you in as this user + // after creation. + Meteor.createUser = function (options, extra, callback) { + + if (typeof extra === "function") { + callback = extra; + extra = {}; + } + + // XXX relax these constraints! + + 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."); + // XXX we don't have a base url, so we don't know how to generate links. + if (options.validation) + throw new Error("Validation email from server not supported yet."); + + + + var userId = createUser(options, extra); + return userId; + }; + + + + })(); diff --git a/packages/accounts-passwords/passwords_tests.js b/packages/accounts-passwords/passwords_tests.js index f8e45c38a1..8f606980d6 100644 --- a/packages/accounts-passwords/passwords_tests.js +++ b/packages/accounts-passwords/passwords_tests.js @@ -1,4 +1,4 @@ -(function () { +if (Meteor.is_client) (function () { // XXX note, only one test can do login/logout things at once! for // now, that is this test. @@ -167,15 +167,41 @@ test.equal(Meteor.user().touchedByOnCreateUser, true); })); }, - // can't call onCreateUserHook twice - function(test, expect) { - Meteor.call('setupMoreThanOneOnCreateUserHook', - {testOnCreateUserHook: true}, expect(function (error) { - test.equal(error.error, 999); - })); - }, logoutStep // XXX test Meteor.accounts.config(unsafePasswordChanges) ]); }) (); + + +if (Meteor.is_server) (function () { + + Tinytest.add( + 'passwords - setup more than one onCreateUserHook', + function (test) { + test.throws(function() { + Meteor.accounts.onCreateUser(function () {}); + }); + }); + + + Tinytest.add( + 'passwords - createUser hooks', + function (test) { + var email = Meteor.uuid() + '@example.com'; + test.throws(function () { + Meteor.createUser({email: email}, + {invalid: true}); // should fail the new user validators + }); + + var userId = Meteor.createUser({email: email}, + {testOnCreateUserHook: true}); + test.isTrue(userId); + var user = Meteor.users.findOne(userId); + test.equal(user.touchedByOnCreateUser, true); + }); + + + + // XXX would be nice to test Meteor.accounts.config({forbidSignups: true}) +}) (); diff --git a/packages/accounts-passwords/passwords_tests_setup.js b/packages/accounts-passwords/passwords_tests_setup.js index ffb4f9dc38..1fdf3ca4f2 100644 --- a/packages/accounts-passwords/passwords_tests_setup.js +++ b/packages/accounts-passwords/passwords_tests_setup.js @@ -11,16 +11,6 @@ Meteor.accounts.onCreateUser(function (options, extra, user) { } }); -Meteor.methods({ - setupMoreThanOneOnCreateUserHook: function () { - try { - Meteor.accounts.onCreateUser(function () {}); - } catch (exception) { - throw new Meteor.Error(999, "Test exception"); - } - } -}); - // Because this is global state that affects every client, we can't turn // it on and off during the tests. Doing so would mean two simultaneous