diff --git a/resources/static/css/style.css b/resources/static/css/style.css index 00b561a12fae1d6333e302dbd391c089012d09ed..c4cbd340cd562874c109408b844df8547a1dc8bf 100644 --- a/resources/static/css/style.css +++ b/resources/static/css/style.css @@ -667,7 +667,7 @@ h1 { margin-bottom: 10px; } -.siteinfo, #congrats, #signUpForm > .password_entry, .enter_password .hint, #unknown_secondary, #primary_verify, .verify_primary .submit { +.siteinfo, #congrats, #signUpForm .password_entry, .enter_password .hint, #unknown_secondary, #primary_verify, .verify_primary .submit { display: none; } @@ -675,7 +675,7 @@ h1 { float: left; } -.enter_password #signUpForm > .password_entry, .known_secondary #signUpForm > .password_entry, +.enter_password #signUpForm .password_entry, .known_secondary #signUpForm .password_entry, .unknown_secondary #unknown_secondary, .verify_primary #verify_primary { display: block; } diff --git a/resources/static/pages/signup.js b/resources/static/pages/signup.js index 475d53824fef0a7c8366995939d6520720d8468e..3fef51badbfe646ad8f2d9ec523afff178dfd896 100644 --- a/resources/static/pages/signup.js +++ b/resources/static/pages/signup.js @@ -12,63 +12,48 @@ BrowserID.signUp = (function() { helpers = bid.Helpers, pageHelpers = bid.PageHelpers, cancelEvent = pageHelpers.cancelEvent, + validation = bid.Validation, errors = bid.Errors, tooltip = BrowserID.Tooltip, ANIMATION_SPEED = 250, storedEmail = pageHelpers, winchan = window.WinChan, - verifyEmail, - verifyURL; + primaryUserInfo, + sc; function showNotice(selector) { $(selector).fadeIn(ANIMATION_SPEED); } function authWithPrimary(oncomplete) { - pageHelpers.openPrimaryAuth(winchan, verifyEmail, verifyURL, primaryAuthComplete); + pageHelpers.openPrimaryAuth(winchan, primaryUserInfo.email, primaryUserInfo.auth, primaryAuthComplete); oncomplete && oncomplete(); } function primaryAuthComplete(error, result, oncomplete) { - if(error) { + if (error) { pageHelpers.showFailure(errors.primaryAuthentication, error, oncomplete); } else { // hey ho, the user is authenticated, re-try the submit. - createUser(verifyEmail, oncomplete); + createPrimaryUser(primaryUserInfo, oncomplete); } } - function createUser(email, oncomplete) { + function createPrimaryUser(info, oncomplete) { function complete(status) { oncomplete && oncomplete(status); } - user.createUser(email, function onComplete(status, info) { + user.createPrimaryUser(info, function onComplete(status, info) { switch(status) { - case "secondary.already_added": - $('#registeredEmail').html(email); - showNotice(".alreadyRegistered"); - complete(false); - break; - case "secondary.verify": - pageHelpers.emailSent(complete); - break; - case "secondary.could_not_add": - tooltip.showTooltip("#could_not_add"); - complete(false); - break; - case "primary.already_added": - // XXX Is this status possible? - break; case "primary.verified": pageHelpers.replaceFormWithNotice("#congrats", complete.bind(null, true)); break; case "primary.verify": - verifyEmail = email; - verifyURL = info.auth; - dom.setInner("#primary_email", email); + primaryUserInfo = info; + dom.setInner("#primary_email", info.email); pageHelpers.replaceInputsWithNotice("#primary_verify", complete.bind(null, false)); break; case "primary.could_not_add": @@ -80,18 +65,62 @@ BrowserID.signUp = (function() { }, pageHelpers.getFailure(errors.createUser, complete)); } - function submit(oncomplete) { - var email = helpers.getAndValidateEmail("#email"); + function enterPasswordState(info) { + var self=this; + self.emailToStage = info.email; + self.submit = passwordSubmit; - function complete(status) { - oncomplete && oncomplete(status); + dom.addClass("body", "enter_password"); + } + + function passwordSubmit(oncomplete) { + var pass = dom.getInner("#password"), + vpass = dom.getInner("#vpassword"), + valid = validation.passwordAndValidationPassword(pass, vpass); + + if(valid) { + user.createSecondaryUser(this.emailToStage, pass, function(status) { + if(status) { + pageHelpers.emailSent(oncomplete && oncomplete.curry(true)); + } + else { + tooltip.showTooltip("#could_not_add"); + oncomplete && oncomplete(false); + } + }, pageHelpers.getFailure(errors.createUser, oncomplete)); + } + else { + oncomplete && oncomplete(false); } + } + + function emailSubmit(oncomplete) { + var email = helpers.getAndValidateEmail("#email"), + self = this; if (email) { - createUser(email, complete); + + user.isEmailRegistered(email, function(isRegistered) { + if(isRegistered) { + $('#registeredEmail').html(email); + showNotice(".alreadyRegistered"); + oncomplete && oncomplete(false); + } + else { + user.addressInfo(email, function(info) { + if(info.type === "primary") { + createPrimaryUser.call(self, info, oncomplete); + } + else { + enterPasswordState.call(self, info); + oncomplete && oncomplete(!isRegistered); + } + }, pageHelpers.getFailure(errors.addressInfo, oncomplete)); + } + }, pageHelpers.getFailure(errors.isEmailRegistered, oncomplete)); } else { - complete(false); + oncomplete && oncomplete(false); } } @@ -103,39 +132,45 @@ BrowserID.signUp = (function() { if (event.which !== 13) $(".notification").fadeOut(ANIMATION_SPEED); } - function init(config) { - config = config || {}; + var Module = bid.Modules.PageModule.extend({ + start: function(options) { + var self=this; + options = options || {}; - if(config.winchan) { - winchan = config.winchan; - } + if (options.winchan) { + winchan = options.winchan; + } - $("form input[autofocus]").focus(); + dom.focus("form input[autofocus]"); - pageHelpers.setupEmail(); + pageHelpers.setupEmail(); + + self.bind("#email", "keyup", onEmailKeyUp); + self.click("#back", back); + self.click("#authWithPrimary", authWithPrimary); + + sc.start.call(self, options); + }, + + submit: emailSubmit, + // BEGIN TESTING API + emailSubmit: emailSubmit, + passwordSubmit: passwordSubmit, + reset: reset, + back: back, + authWithPrimary: authWithPrimary, + primaryAuthComplete: primaryAuthComplete + // END TESTING API + }); - dom.bindEvent("#email", "keyup", onEmailKeyUp); - dom.bindEvent("form", "submit", cancelEvent(submit)); - dom.bindEvent("#back", "click", cancelEvent(back)); - dom.bindEvent("#authWithPrimary", "click", cancelEvent(authWithPrimary)); - } // BEGIN TESTING API function reset() { - dom.unbindEvent("#email", "keyup"); - dom.unbindEvent("form", "submit"); - dom.unbindEvent("#back", "click"); - dom.unbindEvent("#authWithPrimary", "click"); winchan = window.WinChan; - verifyEmail = verifyURL = null; } - - init.submit = submit; - init.reset = reset; - init.back = back; - init.authWithPrimary = authWithPrimary; - init.primaryAuthComplete = primaryAuthComplete; // END TESTING API - return init; + sc = Module.sc; + + return Module; }()); diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js index 61f39797b9e18c1706352e6ba61c6a84c21ea70a..cb9e52e848e9855768f1b519f3620959ba1c5fd2 100644 --- a/resources/static/shared/user.js +++ b/resources/static/shared/user.js @@ -298,15 +298,12 @@ BrowserID.User = (function() { }, /** - * Create a user. Works for both primaries and secondaries. + * Create a primary user. * @method createUser - * @param {string} email + * @param {object} info * @param {function} onComplete - function to call on complettion. Called * with two parameters - status and info. * Status can be: - * secondary.already_added - * secondary.verify - * secondary.could_not_add * primary.already_added * primary.verified * primary.verify @@ -315,63 +312,23 @@ BrowserID.User = (function() { * info is passed on primary.verify and contains the info necessary to * verify the user with the IdP */ - // XXX - only used on main site - createUser: function(email, onComplete, onFailure) { - User.addressInfo(email, function(info) { - User.createUserWithInfo(email, info, onComplete, onFailure); - }, onFailure); - }, - - /** - * Attempt to create a user with the info returned from - * network.addressInfo. Attempts to create both primary and secondary - * based users depending on info.type. - * @method createUserWithInfo - * @param {string} email - * @param {object} info - contains fields returned from network.addressInfo - * @param {function} [onComplete] - * @param {function} [onFailure] - */ - createUserWithInfo: function(email, info, onComplete, onFailure) { - function attemptAddSecondary(email, info) { - if (info.known) { - onComplete("secondary.already_added"); - } - else { - User.createSecondaryUser(email, function(success) { - if (success) { - onComplete("secondary.verify"); + createPrimaryUser: function(info, onComplete, onFailure) { + var email = info.email; + User.provisionPrimaryUser(email, info, function(status, provInfo) { + if (status === "primary.verified") { + network.authenticateWithAssertion(email, provInfo.assertion, function(status) { + if (status) { + onComplete("primary.verified"); } else { - onComplete("secondary.could_not_add"); + onComplete("primary.could_not_add"); } }, onFailure); } - } - - function attemptAddPrimary(email, info) { - User.provisionPrimaryUser(email, info, function(status, provInfo) { - if (status === "primary.verified") { - network.authenticateWithAssertion(email, provInfo.assertion, function(status) { - if (status) { - onComplete("primary.verified"); - } - else { - onComplete("primary.could_not_add"); - } - }, onFailure); - } - else { - onComplete(status, provInfo); - } - }, onFailure); - } - - if (info.type === 'secondary') { - attemptAddSecondary(email, info); - } else { - attemptAddPrimary(email, info); - } + else { + onComplete(status, provInfo); + } + }, onFailure); }, /** diff --git a/resources/static/test/cases/pages/signup.js b/resources/static/test/cases/pages/signup.js index 6fe113b2b6ad8e6c5f08a01da9a3d7bc1733a6f7..92c2cd00573462efdfb27549a73f4711ab56fc7f 100644 --- a/resources/static/test/cases/pages/signup.js +++ b/resources/static/test/cases/pages/signup.js @@ -13,7 +13,8 @@ WinChanMock = bid.Mocks.WinChan, testHelpers = bid.TestHelpers, provisioning = bid.Mocks.Provisioning, - winchan; + winchan, + controller; module("pages/signup", { setup: function() { @@ -22,18 +23,20 @@ $(".emailsent").hide(); $(".notification").hide() winchan = new WinChanMock(); - bid.signUp({ + controller = bid.signUp.create(); + controller.start({ winchan: winchan }); }, teardown: function() { testHelpers.teardown(); - bid.signUp.reset(); + controller.reset(); + controller.destroy(); } }); - function testNotRegistered(extraTests) { - bid.signUp.submit(function(status) { + function testPasswordNotShown(extraTests) { + controller.submit(function(status) { strictEqual(status, false, "address was not registered"); equal($(".emailsent").is(":visible"), false, "email not sent, notice not visible"); @@ -42,64 +45,69 @@ }); } - asyncTest("signup with valid unregistered secondary email", function() { - xhr.useResult("unknown_secondary"); - + asyncTest("signup with valid unregistered secondary email - show password", function() { $("#email").val("unregistered@testuser.com"); - bid.signUp.submit(function() { - equal($(".emailsent").is(":visible"), true, "email sent, notice visible"); + controller.submit(function() { + equal($("body").hasClass("enter_password"), true, "new email, password section shown"); + start(); }); }); - asyncTest("signup with valid unregistered email with leading/trailing whitespace", function() { - xhr.useResult("unknown_secondary"); + asyncTest("submit with valid unregistered email with leading/trailing whitespace", function() { $("#email").val(" unregistered@testuser.com "); - bid.signUp.submit(function() { - equal($(".emailsent").is(":visible"), true, "email sent, notice visible"); + controller.submit(function() { + equal($("body").hasClass("enter_password"), true, "new email, password section shown"); start(); }); }); - asyncTest("signup with valid registered email", function() { - xhr.useResult("known_secondary"); + asyncTest("submit with valid registered email", function() { $("#email").val("registered@testuser.com"); - testNotRegistered(); + testPasswordNotShown(); }); - asyncTest("signup with invalid email address", function() { + asyncTest("submit with invalid email address", function() { $("#email").val("invalid"); - testNotRegistered(); + testPasswordNotShown(); }); - asyncTest("signup with throttling", function() { - xhr.useResult("throttle"); - + asyncTest("submit with XHR error", function() { + xhr.useResult("ajaxError"); $("#email").val("unregistered@testuser.com"); - testNotRegistered(); + testPasswordNotShown(function() { + testHelpers.testErrorVisible(); + }); }); - asyncTest("signup with XHR error", function() { - xhr.useResult("invalid"); + + asyncTest("passwordSubmit with throttling", function() { $("#email").val("unregistered@testuser.com"); + $("#password, #vpassword").val("password"); - testNotRegistered(function() { - testHelpers.testErrorVisible(); + xhr.useResult("throttle"); + controller.passwordSubmit(function(userStaged) { + equal(userStaged, false, "email throttling took effect, user not staged"); + start(); }); }); - asyncTest("signup with unregistered secondary email and cancel button pressed", function() { - xhr.useResult("unknown_secondary"); + asyncTest("passwordSubmit happy case, check back button too", function() { $("#email").val("unregistered@testuser.com"); + $("#password, #vpassword").val("password"); - bid.signUp.submit(function() { - bid.signUp.back(function() { + controller.passwordSubmit(function(userStaged) { + equal(userStaged, true, "user has been staged"); + equal($(".emailsent").is(":visible"), true, "email sent, notice visible"); + + // check back button + controller.back(function() { equal($(".notification:visible").length, 0, "no notifications are visible - visible: " + $(".notification:visible").attr("id")); ok($(".forminputs:visible").length, "form inputs are again visible"); equal($("#email").val(), "unregistered@testuser.com", "email address restored"); @@ -108,6 +116,7 @@ }); }); + asyncTest("signup with primary email address, provisioning failure - expect error screen", function() { xhr.useResult("primary"); $("#email").val("unregistered@testuser.com"); @@ -116,7 +125,7 @@ msg: "doowap" }); - bid.signUp.submit(function(status) { + controller.submit(function(status) { equal(status, false, "provisioning failure, status false"); testHelpers.testErrorVisible(); start(); @@ -128,7 +137,7 @@ $("#email").val("unregistered@testuser.com"); provisioning.setStatus(provisioning.AUTHENTICATED); - bid.signUp.submit(function(status) { + controller.submit(function(status) { equal(status, true, "primary addition success - true status"); equal($("#congrats:visible").length, 1, "success notification is visible"); start(); @@ -139,7 +148,7 @@ xhr.useResult("primary"); $("#email").val("unregistered@testuser.com"); - bid.signUp.submit(function(status) { + controller.submit(function(status) { equal($("#primary_verify:visible").length, 1, "success notification is visible"); equal($("#primary_email").text(), "unregistered@testuser.com", "correct email shown"); equal(status, false, "user must authenticate, some action needed."); @@ -151,8 +160,8 @@ xhr.useResult("primary"); $("#email").val("unregistered@testuser.com"); - bid.signUp.submit(function(status) { - bid.signUp.authWithPrimary(function() { + controller.submit(function(status) { + controller.authWithPrimary(function() { ok(winchan.oncomplete, "winchan set up"); start(); }); @@ -160,7 +169,7 @@ }); asyncTest("primaryAuthComplete with error, expect incorrect status", function() { - bid.signUp.primaryAuthComplete("error", "", function(status) { + controller.primaryAuthComplete("error", "", function(status) { equal(status, false, "correct status for could not complete"); testHelpers.testErrorVisible(); start(); @@ -171,15 +180,15 @@ xhr.useResult("primary"); $("#email").val("unregistered@testuser.com"); - bid.signUp.submit(function(status) { - bid.signUp.authWithPrimary(function() { + controller.submit(function(status) { + controller.authWithPrimary(function() { // In real life the user would now be authenticated. provisioning.setStatus(provisioning.AUTHENTICATED); // Before primaryAuthComplete is called, we reset the user caches to // force re-fetching of what could have been stale user data. user.resetCaches(); - bid.signUp.primaryAuthComplete(null, "success", function(status) { + controller.primaryAuthComplete(null, "success", function(status) { equal(status, true, "correct status"); equal($("#congrats:visible").length, 1, "success notification is visible"); start(); diff --git a/resources/static/test/cases/shared/user.js b/resources/static/test/cases/shared/user.js index d5593afd7277f2ad019fae2d0339d6fd85ce4771..6a27aa825b24a2ae7e0b7cdabeaf98d633c846f8 100644 --- a/resources/static/test/cases/shared/user.js +++ b/resources/static/test/cases/shared/user.js @@ -153,33 +153,11 @@ var vep = require("./vep"); }); - asyncTest("createUser with unknown secondary happy case - expect 'secondary.verify'", function() { - xhr.useResult("unknown_secondary"); - - lib.createUser("unregistered@testuser.com", function(status) { - equal(status, "secondary.verify", "secondary user must be verified"); - start(); - }, testHelpers.unexpectedXHRFailure); - }); - - asyncTest("createUser with unknown secondary, throttled - expect status='secondary.could_not_add'", function() { - xhr.useResult("throttle"); - - lib.createUser("unregistered@testuser.com", function(status) { - equal(status, "secondary.could_not_add", "user creation refused"); - start(); - }, testHelpers.unexpectedXHRFailure); - }); - - asyncTest("createUser with unknown secondary, XHR failure - expect failure call", function() { - failureCheck(lib.createUser, "unregistered@testuser.com"); - }); - - asyncTest("createUser with primary, user verified with primary - expect 'primary.verified'", function() { + asyncTest("createPrimaryUser with primary, user verified with primary - expect 'primary.verified'", function() { xhr.useResult("primary"); provisioning.setStatus(provisioning.AUTHENTICATED); - lib.createUser("unregistered@testuser.com", function(status) { + lib.createPrimaryUser({email: "unregistered@testuser.com"}, function(status) { equal(status, "primary.verified", "primary user is already verified, correct status"); network.checkAuth(function(authenticated) { equal(authenticated, "assertion", "after provisioning user, user should be automatically authenticated to BrowserID"); @@ -188,33 +166,28 @@ var vep = require("./vep"); }, testHelpers.unexpectedXHRFailure); }); - asyncTest("createUser with primary, user must authenticate with primary - expect 'primary.verify'", function() { + asyncTest("createPrimaryUser with primary, user must authenticate with primary - expect 'primary.verify'", function() { xhr.useResult("primary"); - lib.createUser("unregistered@testuser.com", function(status) { + lib.createPrimaryUser({email: "unregistered@testuser.com"}, function(status) { equal(status, "primary.verify", "primary must verify with primary, correct status"); start(); }, testHelpers.unexpectedXHRFailure); }); - asyncTest("createUser with primary, unknown provisioning failure, expect XHR failure callback", function() { + asyncTest("createPrimaryUser with primary, unknown provisioning failure, expect XHR failure callback", function() { xhr.useResult("primary"); provisioning.setFailure({ code: "primaryError", msg: "some error" }); - lib.createUser("unregistered@testuser.com", + lib.createPrimaryUser({email: "unregistered@testuser.com"}, testHelpers.unexpectedSuccess, testHelpers.expectedXHRFailure ); }); - asyncTest("createUserWithInfo", function() { - ok(true, "For development speed and reduced duplication of tests, tested via createUser"); - start(); - }); - asyncTest("provisionPrimaryUser authenticated with IdP, expect primary.verified", function() { xhr.useResult("primary"); provisioning.setStatus(provisioning.AUTHENTICATED); diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js index 17aa6c9851933562e057afbd92c26cb68fa1d4af..d78791805fd24662db2c1e48b7ccdc5a9a4ec96c 100644 --- a/resources/static/test/mocks/xhr.js +++ b/resources/static/test/mocks/xhr.js @@ -68,6 +68,7 @@ BrowserID.Mocks.xhr = (function() { "get /wsapi/have_email?email=registered%40testuser.com throttle": { email_known: true }, "get /wsapi/have_email?email=registered%40testuser.com ajaxError": undefined, "get /wsapi/have_email?email=unregistered%40testuser.com valid": { email_known: false }, + "get /wsapi/have_email?email=unregistered%40testuser.com primary": { email_known: false }, "post /wsapi/remove_email valid": { success: true }, "post /wsapi/remove_email invalid": { success: false }, "post /wsapi/remove_email multiple": { success: true }, @@ -104,6 +105,7 @@ BrowserID.Mocks.xhr = (function() { "post /wsapi/update_password invalid": undefined, "get /wsapi/address_info?email=unregistered%40testuser.com invalid": undefined, "get /wsapi/address_info?email=unregistered%40testuser.com throttle": { type: "secondary", known: false }, + "get /wsapi/address_info?email=unregistered%40testuser.com valid": { type: "secondary", known: false }, "get /wsapi/address_info?email=unregistered%40testuser.com unknown_secondary": { type: "secondary", known: false }, "get /wsapi/address_info?email=registered%40testuser.com known_secondary": { type: "secondary", known: true }, "get /wsapi/address_info?email=registered%40testuser.com primary": { type: "primary", auth: "https://auth_url", prov: "https://prov_url" }, diff --git a/resources/views/signup.ejs b/resources/views/signup.ejs index 51b280608d0900893cf11ab0a38532e94bd25328..6e786017f8f383c8af4415c7b49de23fb741a312 100644 --- a/resources/views/signup.ejs +++ b/resources/views/signup.ejs @@ -50,6 +50,37 @@ We just sent an email to that address! If you really want to send another, wait a minute or two and try again. </div> </li> + + <li class="password_entry"> + <label class="serif" for="password">Password</label> + <input class="sans" id="password" placeholder="Password" type="password" tabindex="2" maxlength="80"> + + <div id="password_required" class="tooltip" for="password"> + Password is required. + </div> + + <div class="tooltip" id="password_length" for="password"> + Password must be between 8 and 80 characters long. + </div> + + <div id="could_not_add" class="tooltip" for="password"> + We just sent an email to that address! If you really want to send another, wait a minute or two and try again. + </div> + </li> + + <li class="password_entry"> + <label class="serif" for="vpassword">Verify Password</label> + <input class="sans" id="vpassword" placeholder="Repeat Password" type="password" tabindex="2" maxlength="80"> + + <div id="password_required" class="tooltip" for="vpassword"> + Verification password is required. + </div> + + <div class="tooltip" id="passwords_no_match" for="vpassword"> + Passwords do not match. + </div> + + </li> </ul> <div class="submit cf forminputs">