diff --git a/app/assets/javascripts/discourse/app/controllers/create-account.js b/app/assets/javascripts/discourse/app/controllers/create-account.js index a74aa542e3b..f1fd1c66a63 100644 --- a/app/assets/javascripts/discourse/app/controllers/create-account.js +++ b/app/assets/javascripts/discourse/app/controllers/create-account.js @@ -20,6 +20,7 @@ import { userPath } from "discourse/lib/url"; import { findAll } from "discourse/models/login-method"; import EmberObject from "@ember/object"; import User from "discourse/models/user"; +import { Promise } from "rsvp"; export default Controller.extend( ModalFunctionality, @@ -41,9 +42,13 @@ export default Controller.extend( hasAuthOptions: notEmpty("authOptions"), canCreateLocal: setting("enable_local_logins"), - showCreateForm: or("hasAuthOptions", "canCreateLocal"), requireInviteCode: setting("require_invite_code"), + @discourseComputed("hasAuthOptions", "canCreateLocal", "skipConfirmation") + showCreateForm(hasAuthOptions, canCreateLocal, skipConfirmation) { + return (hasAuthOptions || canCreateLocal) && !skipConfirmation; + }, + resetForm() { // We wrap the fields in a structure so we can assign a value this.setProperties({ @@ -197,26 +202,44 @@ export default Controller.extend( @on("init") fetchConfirmationValue() { - return ajax(userPath("hp.json")).then(json => { - this._challengeDate = new Date(); - // remove 30 seconds for jitter, make sure this works for at least - // 30 seconds so we don't have hard loops - this._challengeExpiry = parseInt(json.expires_in, 10) - 30; - if (this._challengeExpiry < 30) { - this._challengeExpiry = 30; - } + if (this._challengeDate === undefined && this._hpPromise) { + // Request already in progress + return this._hpPromise; + } - this.setProperties({ - accountHoneypot: json.value, - accountChallenge: json.challenge - .split("") - .reverse() - .join("") - }); - }); + this._hpPromise = ajax(userPath("hp.json")) + .then(json => { + this._challengeDate = new Date(); + // remove 30 seconds for jitter, make sure this works for at least + // 30 seconds so we don't have hard loops + this._challengeExpiry = parseInt(json.expires_in, 10) - 30; + if (this._challengeExpiry < 30) { + this._challengeExpiry = 30; + } + + this.setProperties({ + accountHoneypot: json.value, + accountChallenge: json.challenge + .split("") + .reverse() + .join("") + }); + }) + .finally(() => (this._hpPromise = undefined)); + + return this._hpPromise; }, performAccountCreation() { + if ( + !this._challengeDate || + new Date() - this._challengeDate > 1000 * this._challengeExpiry + ) { + return this.fetchConfirmationValue().then(() => + this.performAccountCreation() + ); + } + const attrs = this.getProperties( "accountName", "accountEmail", @@ -263,6 +286,7 @@ export default Controller.extend( .find("input[name=redirect]") .val(userPath("account-created")); $hidden_login_form.submit(); + return new Promise(() => {}); // This will never resolve, the page will reload instead } else { this.flash( result.message || I18n.t("create_account.failed"), @@ -298,6 +322,14 @@ export default Controller.extend( ); }, + onShow() { + if (this.skipConfirmation) { + this.performAccountCreation().finally(() => + this.set("skipConfirmation", false) + ); + } + }, + actions: { externalLogin(provider) { this.login.send("externalLogin", provider); @@ -332,13 +364,7 @@ export default Controller.extend( return; } - if (new Date() - this._challengeDate > 1000 * this._challengeExpiry) { - this.fetchConfirmationValue().then(() => - this.performAccountCreation() - ); - } else { - this.performAccountCreation(); - } + this.performAccountCreation(); } } } diff --git a/app/assets/javascripts/discourse/app/controllers/login.js b/app/assets/javascripts/discourse/app/controllers/login.js index 36e686d6106..870c75d9000 100644 --- a/app/assets/javascripts/discourse/app/controllers/login.js +++ b/app/assets/javascripts/discourse/app/controllers/login.js @@ -381,12 +381,16 @@ export default Controller.extend(ModalFunctionality, { return; } + const skipConfirmation = + options && this.siteSettings.external_auth_skip_create_confirm; + const createAccountController = this.createAccount; createAccountController.setProperties({ accountEmail: options.email, accountUsername: options.username, accountName: options.name, - authOptions: EmberObject.create(options) + authOptions: EmberObject.create(options), + skipConfirmation }); showModal("createAccount", { modalClass: "create-account" }); diff --git a/app/assets/javascripts/discourse/app/templates/modal/create-account.hbs b/app/assets/javascripts/discourse/app/templates/modal/create-account.hbs index eb2c7940809..2edb94b483e 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/create-account.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/create-account.hbs @@ -7,6 +7,10 @@ {{login-buttons externalLogin=(action "externalLogin")}} {{/unless}} + {{#if skipConfirmation}} + {{loading-spinner size="large"}} + {{/if}} + {{#if showCreateForm}}
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 1b039512fa0..8a9c42ff48b 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1624,6 +1624,8 @@ en: password_unique_characters: "Minimum number of unique characters that a password must have." block_common_passwords: "Don't allow passwords that are in the 10,000 most common passwords." + external_auth_skip_create_confirm: When signing up via external auth, skip the create account popup. Best used alongside sso_overrides_email, sso_overrides_username and sso_overrides_name. + enable_sso: "Enable single sign on via an external site (WARNING: USERS' EMAIL ADDRESSES *MUST* BE VALIDATED BY THE EXTERNAL SITE!)" verbose_sso_logging: "Log verbose SSO related diagnostics to /logs" enable_sso_provider: "Implement Discourse SSO provider protocol at the /session/sso_provider endpoint, requires sso_provider_secrets to be set" diff --git a/config/site_settings.yml b/config/site_settings.yml index 98ddb57faab..ecaf08dd4b1 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -379,6 +379,9 @@ login: discord_trusted_guilds: default: "" type: list + external_auth_skip_create_confirm: + default: false + client: true enable_sso: client: true default: false diff --git a/test/javascripts/acceptance/create-account-external-test.js b/test/javascripts/acceptance/create-account-external-test.js new file mode 100644 index 00000000000..cd64fe1ec24 --- /dev/null +++ b/test/javascripts/acceptance/create-account-external-test.js @@ -0,0 +1,43 @@ +import { acceptance } from "helpers/qunit-helpers"; + +acceptance("Create Account - external auth", { + beforeEach() { + const node = document.createElement("meta"); + node.dataset.authenticationData = JSON.stringify({ + auth_provider: "test", + email: "blah@example.com", + can_edit_username: true, + can_edit_name: true + }); + node.id = "data-authentication"; + document.querySelector("head").appendChild(node); + }, + afterEach() { + document + .querySelector("head") + .removeChild(document.getElementById("data-authentication")); + } +}); + +QUnit.test("when skip is disabled (default)", async assert => { + await visit("/"); + + assert.ok( + exists("#discourse-modal div.create-account"), + "it shows the registration modal" + ); + + assert.ok(exists("#new-account-username"), "it shows the fields"); +}); + +QUnit.test("when skip is enabled", async assert => { + Discourse.SiteSettings.external_auth_skip_create_confirm = true; + await visit("/"); + + assert.ok( + exists("#discourse-modal div.create-account"), + "it shows the registration modal" + ); + + assert.not(exists("#new-account-username"), "it does not show the fields"); +});