FIX: Automatic auth flow with full page login/signup (#30928)

Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
This commit is contained in:
Jan Cernik
2025-01-23 08:43:07 -03:00
committed by GitHub
parent f23004c626
commit 13f86c99ea
8 changed files with 520 additions and 444 deletions

View File

@ -140,14 +140,18 @@ export default {
}; };
if (siteSettings.full_page_login) { if (siteSettings.full_page_login) {
router.transitionTo("signup").then((signup) => { router
const signupController = .transitionTo("signup", {
signup.controller || owner.lookup("controller:signup"); queryParams: { authComplete: true },
Object.keys(createAccountProps || {}).forEach((key) => { })
signupController.set(key, createAccountProps[key]); .then((signup) => {
const signupController =
signup.controller || owner.lookup("controller:signup");
Object.keys(createAccountProps || {}).forEach((key) => {
signupController.set(key, createAccountProps[key]);
});
signupController.handleSkipConfirmation();
}); });
signupController.handleSkipConfirmation();
});
} else { } else {
modal.show(CreateAccount, { model: createAccountProps }); modal.show(CreateAccount, { model: createAccountProps });
} }

View File

@ -17,7 +17,6 @@ import DiscourseURL from "discourse/lib/url";
import { postRNWebviewMessage } from "discourse/lib/utilities"; import { postRNWebviewMessage } from "discourse/lib/utilities";
import Category from "discourse/models/category"; import Category from "discourse/models/category";
import Composer from "discourse/models/composer"; import Composer from "discourse/models/composer";
import { findAll } from "discourse/models/login-method";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
@ -44,17 +43,6 @@ export default class ApplicationRoute extends DiscourseRoute {
@setting("title") siteTitle; @setting("title") siteTitle;
@setting("short_site_description") shortSiteDescription; @setting("short_site_description") shortSiteDescription;
get isOnlyOneExternalLoginMethod() {
return (
!this.siteSettings.enable_local_logins &&
this.externalLoginMethods.length === 1
);
}
get externalLoginMethods() {
return findAll();
}
@action @action
loading(transition) { loading(transition) {
this.loadingSlider.transitionStarted(); this.loadingSlider.transitionStarted();
@ -295,8 +283,8 @@ export default class ApplicationRoute extends DiscourseRoute {
: encodeURIComponent(window.location.pathname); : encodeURIComponent(window.location.pathname);
window.location = getURL("/session/sso?return_path=" + returnPath); window.location = getURL("/session/sso?return_path=" + returnPath);
} else { } else {
if (this.isOnlyOneExternalLoginMethod) { if (this.login.isOnlyOneExternalLoginMethod) {
this.login.externalLogin(this.externalLoginMethods[0]); this.login.singleExternalLogin();
} else if (this.siteSettings.full_page_login) { } else if (this.siteSettings.full_page_login) {
this.router.transitionTo("login").then((login) => { this.router.transitionTo("login").then((login) => {
login.controller.set("canSignUp", this.controller.canSignUp); login.controller.set("canSignUp", this.controller.canSignUp);
@ -321,11 +309,9 @@ export default class ApplicationRoute extends DiscourseRoute {
const returnPath = encodeURIComponent(window.location.pathname); const returnPath = encodeURIComponent(window.location.pathname);
window.location = getURL("/session/sso?return_path=" + returnPath); window.location = getURL("/session/sso?return_path=" + returnPath);
} else { } else {
if (this.isOnlyOneExternalLoginMethod) { if (this.login.isOnlyOneExternalLoginMethod) {
// we will automatically redirect to the external auth service // we will automatically redirect to the external auth service
this.login.externalLogin(this.externalLoginMethods[0], { this.login.singleExternalLogin({ signup: true });
signup: true,
});
} else if (this.siteSettings.full_page_login) { } else if (this.siteSettings.full_page_login) {
this.router.transitionTo("signup").then((signup) => { this.router.transitionTo("signup").then((signup) => {
Object.keys(createAccountProps || {}).forEach((key) => { Object.keys(createAccountProps || {}).forEach((key) => {

View File

@ -7,9 +7,12 @@ import DiscourseRoute from "discourse/routes/discourse";
export default class LoginRoute extends DiscourseRoute { export default class LoginRoute extends DiscourseRoute {
@service siteSettings; @service siteSettings;
@service router; @service router;
@service login;
beforeModel() { beforeModel() {
if ( if (this.login.isOnlyOneExternalLoginMethod) {
this.login.singleExternalLogin();
} else if (
!this.siteSettings.login_required && !this.siteSettings.login_required &&
(!this.siteSettings.full_page_login || (!this.siteSettings.full_page_login ||
this.siteSettings.enable_discourse_connect) this.siteSettings.enable_discourse_connect)
@ -35,6 +38,10 @@ export default class LoginRoute extends DiscourseRoute {
controller.set("flashType", ""); controller.set("flashType", "");
controller.set("flash", ""); controller.set("flash", "");
if (this.login.isOnlyOneExternalLoginMethod) {
controller.set("isRedirecting", true);
}
if (this.siteSettings.login_required) { if (this.siteSettings.login_required) {
controller.set("showLogin", false); controller.set("showLogin", false);
} }

View File

@ -4,11 +4,28 @@ import { service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
export default class SignupRoute extends DiscourseRoute { export default class SignupRoute extends DiscourseRoute {
@service router;
@service siteSettings; @service siteSettings;
@service router;
@service login;
beforeModel() { authComplete = false;
this.showCreateAccount();
beforeModel(transition) {
this.authComplete = transition.to.queryParams.authComplete || false;
if (this.login.isOnlyOneExternalLoginMethod && !this.authComplete) {
this.login.singleExternalLogin({ signup: true });
} else {
this.showCreateAccount();
}
}
setupController(controller) {
super.setupController(...arguments);
if (this.login.isOnlyOneExternalLoginMethod && !this.authComplete) {
controller.set("isRedirecting", true);
}
} }
@action @action

View File

@ -1,9 +1,12 @@
import { action } from "@ember/object"; import { action } from "@ember/object";
import Service from "@ember/service"; import Service, { service } from "@ember/service";
import { disableImplicitInjections } from "discourse/lib/implicit-injections"; import { disableImplicitInjections } from "discourse/lib/implicit-injections";
import { findAll } from "discourse/models/login-method";
@disableImplicitInjections @disableImplicitInjections
export default class LoginService extends Service { export default class LoginService extends Service {
@service siteSettings;
@action @action
async externalLogin( async externalLogin(
loginMethod, loginMethod,
@ -16,4 +19,20 @@ export default class LoginService extends Service {
setLoggingIn?.(false); setLoggingIn?.(false);
} }
} }
@action
async singleExternalLogin(opts) {
await this.externalLogin(this.externalLoginMethods[0], opts);
}
get isOnlyOneExternalLoginMethod() {
return (
!this.siteSettings.enable_local_logins &&
this.externalLoginMethods.length === 1
);
}
get externalLoginMethods() {
return findAll();
}
} }

View File

@ -1,176 +1,183 @@
{{#if {{hide-application-sidebar}}
(and {{body-class "login-page"}}
this.siteSettings.full_page_login
(or this.showLogin (not this.siteSettings.login_required))
)
}}
{{hide-application-header-buttons "search" "login" "signup" "menu"}}
{{hide-application-sidebar}}
{{body-class "login-page"}}
<div class="login-fullpage">
<FlashMessage @flash={{this.flash}} @type={{this.flashType}} />
<div class={{concat-class "login-body" this.bodyClasses}}> {{#if this.isRedirecting}}
<PluginOutlet @name="login-before-modal-body" @connectorTagName="div" /> {{loading-spinner}}
{{else}}
{{#if
(and
this.siteSettings.full_page_login
(or this.showLogin (not this.siteSettings.login_required))
)
}}
{{hide-application-header-buttons "search" "login" "signup" "menu"}}
<div class="login-fullpage">
<FlashMessage @flash={{this.flash}} @type={{this.flashType}} />
{{#if this.hasNoLoginOptions}} <div class={{concat-class "login-body" this.bodyClasses}}>
<div class={{if this.site.desktopView "login-left-side"}}> <PluginOutlet @name="login-before-modal-body" @connectorTagName="div" />
<div class="login-welcome-header no-login-methods-configured">
<h1 class="login-title">{{i18n "login.no_login_methods.title"}}</h1>
<img />
<p class="login-subheader">
{{html-safe
(i18n
"login.no_login_methods.description"
(hash adminLoginPath=this.adminLoginPath)
)
}}
</p>
</div>
</div>
{{else}}
{{#if this.site.mobileView}}
<WelcomeHeader @header={{i18n "login.header_title"}}>
<PluginOutlet
@name="login-header-bottom"
@outletArgs={{hash createAccount=this.createAccount}}
/>
</WelcomeHeader>
{{#if this.showLoginButtons}}
<LoginButtons
@externalLogin={{this.externalLoginAction}}
@passkeyLogin={{this.passkeyLogin}}
@context="login"
/>
{{/if}}
{{/if}}
{{#if this.canLoginLocal}} {{#if this.hasNoLoginOptions}}
<div class={{if this.site.desktopView "login-left-side"}}> <div class={{if this.site.desktopView "login-left-side"}}>
{{#if this.site.desktopView}} <div class="login-welcome-header no-login-methods-configured">
<WelcomeHeader @header={{i18n "login.header_title"}}> <h1 class="login-title">{{i18n
<PluginOutlet "login.no_login_methods.title"
@name="login-header-bottom" }}</h1>
@outletArgs={{hash createAccount=this.createAccount}} <img />
/> <p class="login-subheader">
</WelcomeHeader> {{html-safe
{{/if}} (i18n
<LocalLoginForm "login.no_login_methods.description"
@loginName={{this.loginName}} (hash adminLoginPath=this.adminLoginPath)
@loginNameChanged={{this.loginNameChanged}} )
@canLoginLocalWithEmail={{this.canLoginLocalWithEmail}} }}
@canUsePasskeys={{this.canUsePasskeys}} </p>
@passkeyLogin={{this.passkeyLogin}}
@loginPassword={{this.loginPassword}}
@secondFactorMethod={{this.secondFactorMethod}}
@secondFactorToken={{this.secondFactorToken}}
@backupEnabled={{this.backupEnabled}}
@totpEnabled={{this.totpEnabled}}
@securityKeyAllowedCredentialIds={{this.securityKeyAllowedCredentialIds}}
@securityKeyChallenge={{this.securityKeyChallenge}}
@showSecurityKey={{this.showSecurityKey}}
@otherMethodAllowed={{this.otherMethodAllowed}}
@showSecondFactor={{this.showSecondFactor}}
@handleForgotPassword={{this.handleForgotPassword}}
@login={{this.triggerLogin}}
@flashChanged={{this.flashChanged}}
@flashTypeChanged={{this.flashTypeChanged}}
@securityKeyCredentialChanged={{this.securityKeyCredentialChanged}}
/>
{{#if this.site.desktopView}}
<LoginPageCta
@canLoginLocal={{this.canLoginLocal}}
@showSecurityKey={{this.showSecurityKey}}
@login={{this.triggerLogin}}
@loginButtonLabel={{this.loginButtonLabel}}
@loginDisabled={{this.loginDisabled}}
@showSignupLink={{this.showSignupLink}}
@createAccount={{this.createAccount}}
@loggingIn={{this.loggingIn}}
@showSecondFactor={{this.showSecondFactor}}
/>
{{/if}}
</div>
{{/if}}
{{#if (and this.showLoginButtons this.site.desktopView)}}
{{#unless this.canLoginLocal}}
<div class="login-left-side">
<WelcomeHeader @header={{i18n "login.header_title"}} />
</div> </div>
{{/unless}} </div>
{{#if this.hasAtLeastOneLoginButton}} {{else}}
<div class="login-right-side"> {{#if this.site.mobileView}}
<WelcomeHeader @header={{i18n "login.header_title"}}>
<PluginOutlet
@name="login-header-bottom"
@outletArgs={{hash createAccount=this.createAccount}}
/>
</WelcomeHeader>
{{#if this.showLoginButtons}}
<LoginButtons <LoginButtons
@externalLogin={{this.externalLoginAction}} @externalLogin={{this.externalLoginAction}}
@passkeyLogin={{this.passkeyLogin}} @passkeyLogin={{this.passkeyLogin}}
@context="login" @context="login"
/> />
{{/if}}
{{/if}}
{{#if this.canLoginLocal}}
<div class={{if this.site.desktopView "login-left-side"}}>
{{#if this.site.desktopView}}
<WelcomeHeader @header={{i18n "login.header_title"}}>
<PluginOutlet
@name="login-header-bottom"
@outletArgs={{hash createAccount=this.createAccount}}
/>
</WelcomeHeader>
{{/if}}
<LocalLoginForm
@loginName={{this.loginName}}
@loginNameChanged={{this.loginNameChanged}}
@canLoginLocalWithEmail={{this.canLoginLocalWithEmail}}
@canUsePasskeys={{this.canUsePasskeys}}
@passkeyLogin={{this.passkeyLogin}}
@loginPassword={{this.loginPassword}}
@secondFactorMethod={{this.secondFactorMethod}}
@secondFactorToken={{this.secondFactorToken}}
@backupEnabled={{this.backupEnabled}}
@totpEnabled={{this.totpEnabled}}
@securityKeyAllowedCredentialIds={{this.securityKeyAllowedCredentialIds}}
@securityKeyChallenge={{this.securityKeyChallenge}}
@showSecurityKey={{this.showSecurityKey}}
@otherMethodAllowed={{this.otherMethodAllowed}}
@showSecondFactor={{this.showSecondFactor}}
@handleForgotPassword={{this.handleForgotPassword}}
@login={{this.triggerLogin}}
@flashChanged={{this.flashChanged}}
@flashTypeChanged={{this.flashTypeChanged}}
@securityKeyCredentialChanged={{this.securityKeyCredentialChanged}}
/>
{{#if this.site.desktopView}}
<LoginPageCta
@canLoginLocal={{this.canLoginLocal}}
@showSecurityKey={{this.showSecurityKey}}
@login={{this.triggerLogin}}
@loginButtonLabel={{this.loginButtonLabel}}
@loginDisabled={{this.loginDisabled}}
@showSignupLink={{this.showSignupLink}}
@createAccount={{this.createAccount}}
@loggingIn={{this.loggingIn}}
@showSecondFactor={{this.showSecondFactor}}
/>
{{/if}}
</div> </div>
{{/if}} {{/if}}
{{/if}}
{{/if}}
{{#if this.site.mobileView}} {{#if (and this.showLoginButtons this.site.desktopView)}}
{{#unless this.hasNoLoginOptions}} {{#unless this.canLoginLocal}}
<LoginPageCta <div class="login-left-side">
@canLoginLocal={{this.canLoginLocal}} <WelcomeHeader @header={{i18n "login.header_title"}} />
@showSecurityKey={{this.showSecurityKey}} </div>
@login={{this.triggerLogin}} {{/unless}}
@loginButtonLabel={{this.loginButtonLabel}} {{#if this.hasAtLeastOneLoginButton}}
@loginDisabled={{this.loginDisabled}} <div class="login-right-side">
@showSignupLink={{this.showSignupLink}} <LoginButtons
@createAccount={{this.createAccount}} @externalLogin={{this.externalLoginAction}}
@loggingIn={{this.loggingIn}} @passkeyLogin={{this.passkeyLogin}}
@showSecondFactor={{this.showSecondFactor}} @context="login"
/> />
{{/unless}} </div>
{{/if}} {{/if}}
</div>
</div>
{{else}}
{{body-class "static-login"}}
<section class="container">
<div class="contents clearfix body-page">
<div class="login-welcome">
<PluginOutlet
@name="above-login"
@outletArgs={{hash model=this.model}}
/>
<PluginOutlet @name="above-static" />
<div class="login-content">
{{html-safe this.model.html}}
</div>
<PluginOutlet @name="below-static" />
<PluginOutlet
@name="below-login"
@outletArgs={{hash model=this.model}}
/>
<div class="body-page-button-container">
{{#if this.application.canSignUp}}
<DButton
@action={{route-action "showCreateAccount"}}
@label="sign_up"
class="btn-primary sign-up-button"
/>
{{/if}} {{/if}}
{{/if}}
<DButton {{#if this.site.mobileView}}
@action={{if {{#unless this.hasNoLoginOptions}}
this.shouldTriggerRouteAction <LoginPageCta
(route-action "showLogin") @canLoginLocal={{this.canLoginLocal}}
this.showFullPageLogin @showSecurityKey={{this.showSecurityKey}}
}} @login={{this.triggerLogin}}
@icon="user" @loginButtonLabel={{this.loginButtonLabel}}
@label="log_in" @loginDisabled={{this.loginDisabled}}
class="btn-primary login-button" @showSignupLink={{this.showSignupLink}}
/> @createAccount={{this.createAccount}}
</div> @loggingIn={{this.loggingIn}}
@showSecondFactor={{this.showSecondFactor}}
/>
{{/unless}}
{{/if}}
</div> </div>
</div> </div>
</section>
{{else}}
{{body-class "static-login"}}
<section class="container">
<div class="contents clearfix body-page">
<div class="login-welcome">
<PluginOutlet
@name="above-login"
@outletArgs={{hash model=this.model}}
/>
<PluginOutlet @name="above-static" />
<div class="login-content">
{{html-safe this.model.html}}
</div>
<PluginOutlet @name="below-static" />
<PluginOutlet
@name="below-login"
@outletArgs={{hash model=this.model}}
/>
<div class="body-page-button-container">
{{#if this.application.canSignUp}}
<DButton
@action={{route-action "showCreateAccount"}}
@label="sign_up"
class="btn-primary sign-up-button"
/>
{{/if}}
<DButton
@action={{if
this.shouldTriggerRouteAction
(route-action "showLogin")
this.showFullPageLogin
}}
@icon="user"
@label="log_in"
class="btn-primary login-button"
/>
</div>
</div>
</div>
</section>
{{/if}}
{{/if}} {{/if}}

View File

@ -2,286 +2,294 @@
{{hide-application-header-buttons "search" "login" "signup" "menu"}} {{hide-application-header-buttons "search" "login" "signup" "menu"}}
{{hide-application-sidebar}} {{hide-application-sidebar}}
{{body-class "signup-page"}} {{body-class "signup-page"}}
<div class="signup-fullpage">
<FlashMessage @flash={{this.flash}} @type={{this.flashType}} />
<div class={{concat-class "signup-body" this.bodyClasses}}> {{#if this.isRedirecting}}
<PluginOutlet {{loading-spinner}}
@name="create-account-before-modal-body" {{else}}
@connectorTagName="div" <div class="signup-fullpage">
/> <FlashMessage @flash={{this.flash}} @type={{this.flashType}} />
<div <div class={{concat-class "signup-body" this.bodyClasses}}>
class={{concat-class <PluginOutlet
(if this.site.desktopView "login-left-side") @name="create-account-before-modal-body"
this.authOptions.auth_provider @connectorTagName="div"
}} />
>
<SignupProgressBar @step="signup" /> <div
<WelcomeHeader class={{concat-class
id="create-account-title" (if this.site.desktopView "login-left-side")
@header={{i18n "create_account.header_title"}} this.authOptions.auth_provider
}}
> >
<PluginOutlet <SignupProgressBar @step="signup" />
@name="create-account-header-bottom" <WelcomeHeader
@outletArgs={{hash showLogin=(route-action "showLogin")}} id="create-account-title"
/> @header={{i18n "create_account.header_title"}}
</WelcomeHeader> >
{{#if this.showCreateForm}}
<form id="login-form">
{{#if this.associateHtml}}
<div class="input-group create-account-associate-link">
<span>{{html-safe this.associateHtml}}</span>
</div>
{{/if}}
<div class="input-group create-account-email">
<Input
{{on "focusout" this.checkEmailAvailability}}
{{on "focusin" this.scrollInputIntoView}}
@type="email"
@value={{this.accountEmail}}
disabled={{this.emailDisabled}}
autofocus="autofocus"
aria-describedby="account-email-validation account-email-validation-more-info"
aria-invalid={{this.emailValidation.failed}}
name="email"
id="new-account-email"
class={{value-entered this.accountEmail}}
/>
<label class="alt-placeholder" for="new-account-email">
{{i18n "user.email.title"}}
</label>
{{#if this.showEmailValidation}}
<InputTip
@validation={{this.emailValidation}}
id="account-email-validation"
/>
{{else}}
<span class="more-info" id="account-email-validation-more-info">
{{#if this.siteSettings.show_signup_form_email_instructions}}
{{i18n "user.email.instructions"}}
{{/if}}
</span>
{{/if}}
</div>
<div class="input-group create-account__username">
<Input
{{on "focusin" this.scrollInputIntoView}}
@value={{this.accountUsername}}
disabled={{this.usernameDisabled}}
maxlength={{this.maxUsernameLength}}
aria-describedby="username-validation username-validation-more-info"
aria-invalid={{this.usernameValidation.failed}}
autocomplete="off"
name="username"
id="new-account-username"
class={{value-entered this.accountUsername}}
/>
<label class="alt-placeholder" for="new-account-username">
{{i18n "user.username.title"}}
</label>
{{#if this.showUsernameInstructions}}
<span class="more-info" id="username-validation-more-info">
{{i18n "user.username.instructions"}}
</span>
{{else}}
<InputTip
@validation={{this.usernameValidation}}
id="username-validation"
/>
{{/if}}
</div>
{{#if (and this.showFullname this.fullnameRequired)}}
<FullnameInput
@nameValidation={{this.nameValidation}}
@nameTitle={{this.nameTitle}}
@accountName={{this.accountName}}
@nameDisabled={{this.nameDisabled}}
@onFocusIn={{this.scrollInputIntoView}}
class="input-group create-account__fullname required"
/>
{{/if}}
<PluginOutlet <PluginOutlet
@name="create-account-before-password" @name="create-account-header-bottom"
@outletArgs={{hash @outletArgs={{hash showLogin=(route-action "showLogin")}}
accountName=this.accountName
accountUsername=this.accountUsername
accountPassword=this.accountPassword
userFields=this.userFields
authOptions=this.authOptions
}}
/> />
</WelcomeHeader>
<div class="input-group create-account__password"> {{#if this.showCreateForm}}
{{#if this.passwordRequired}} <form id="login-form">
<PasswordField {{#if this.associateHtml}}
{{on "focusout" this.togglePasswordValidation}} <div class="input-group create-account-associate-link">
<span>{{html-safe this.associateHtml}}</span>
</div>
{{/if}}
<div class="input-group create-account-email">
<Input
{{on "focusout" this.checkEmailAvailability}}
{{on "focusin" this.scrollInputIntoView}} {{on "focusin" this.scrollInputIntoView}}
@value={{this.accountPassword}} @type="email"
@capsLockOn={{this.capsLockOn}} @value={{this.accountEmail}}
type={{if this.maskPassword "password" "text"}} disabled={{this.emailDisabled}}
autocomplete="current-password" autofocus="autofocus"
aria-describedby="password-validation password-validation-more-info" aria-describedby="account-email-validation account-email-validation-more-info"
aria-invalid={{this.passwordValidation.failed}} aria-invalid={{this.emailValidation.failed}}
id="new-account-password" name="email"
class={{value-entered this.accountPassword}} id="new-account-email"
class={{value-entered this.accountEmail}}
/> />
<label class="alt-placeholder" for="new-account-password"> <label class="alt-placeholder" for="new-account-email">
{{i18n "user.password.title"}} {{i18n "user.email.title"}}
</label> </label>
<TogglePasswordMask {{#if this.showEmailValidation}}
@maskPassword={{this.maskPassword}} <InputTip
@togglePasswordMask={{this.togglePasswordMask}} @validation={{this.emailValidation}}
/> id="account-email-validation"
<div class="create-account__password-info"> />
<div class="create-account__password-tip-validation"> {{else}}
{{#if this.showPasswordValidation}} <span class="more-info" id="account-email-validation-more-info">
<InputTip {{#if this.siteSettings.show_signup_form_email_instructions}}
@validation={{this.passwordValidation}} {{i18n "user.email.instructions"}}
id="password-validation"
/>
{{else if
this.siteSettings.show_signup_form_password_instructions
}}
<span class="more-info" id="password-validation-more-info">
{{this.passwordInstructions}}
</span>
{{/if}} {{/if}}
<div </span>
class={{concat-class {{/if}}
"caps-lock-warning" </div>
(unless this.capsLockOn "hidden")
<div class="input-group create-account__username">
<Input
{{on "focusin" this.scrollInputIntoView}}
@value={{this.accountUsername}}
disabled={{this.usernameDisabled}}
maxlength={{this.maxUsernameLength}}
aria-describedby="username-validation username-validation-more-info"
aria-invalid={{this.usernameValidation.failed}}
autocomplete="off"
name="username"
id="new-account-username"
class={{value-entered this.accountUsername}}
/>
<label class="alt-placeholder" for="new-account-username">
{{i18n "user.username.title"}}
</label>
{{#if this.showUsernameInstructions}}
<span class="more-info" id="username-validation-more-info">
{{i18n "user.username.instructions"}}
</span>
{{else}}
<InputTip
@validation={{this.usernameValidation}}
id="username-validation"
/>
{{/if}}
</div>
{{#if (and this.showFullname this.fullnameRequired)}}
<FullnameInput
@nameValidation={{this.nameValidation}}
@nameTitle={{this.nameTitle}}
@accountName={{this.accountName}}
@nameDisabled={{this.nameDisabled}}
@onFocusIn={{this.scrollInputIntoView}}
class="input-group create-account__fullname required"
/>
{{/if}}
<PluginOutlet
@name="create-account-before-password"
@outletArgs={{hash
accountName=this.accountName
accountUsername=this.accountUsername
accountPassword=this.accountPassword
userFields=this.userFields
authOptions=this.authOptions
}}
/>
<div class="input-group create-account__password">
{{#if this.passwordRequired}}
<PasswordField
{{on "focusout" this.togglePasswordValidation}}
{{on "focusin" this.scrollInputIntoView}}
@value={{this.accountPassword}}
@capsLockOn={{this.capsLockOn}}
type={{if this.maskPassword "password" "text"}}
autocomplete="current-password"
aria-describedby="password-validation password-validation-more-info"
aria-invalid={{this.passwordValidation.failed}}
id="new-account-password"
class={{value-entered this.accountPassword}}
/>
<label class="alt-placeholder" for="new-account-password">
{{i18n "user.password.title"}}
</label>
<TogglePasswordMask
@maskPassword={{this.maskPassword}}
@togglePasswordMask={{this.togglePasswordMask}}
/>
<div class="create-account__password-info">
<div class="create-account__password-tip-validation">
{{#if this.showPasswordValidation}}
<InputTip
@validation={{this.passwordValidation}}
id="password-validation"
/>
{{else if
this.siteSettings.show_signup_form_password_instructions
}} }}
> <span
{{d-icon "triangle-exclamation"}} class="more-info"
{{i18n "login.caps_lock_warning"}} id="password-validation-more-info"
>
{{this.passwordInstructions}}
</span>
{{/if}}
<div
class={{concat-class
"caps-lock-warning"
(unless this.capsLockOn "hidden")
}}
>
{{d-icon "triangle-exclamation"}}
{{i18n "login.caps_lock_warning"}}
</div>
</div> </div>
</div> </div>
{{/if}}
<div class="password-confirmation">
<label for="new-account-password-confirmation">
{{i18n "user.password_confirmation.title"}}
</label>
<HoneypotInput
@id="new-account-confirmation"
@autocomplete="new-password"
@value={{this.accountHoneypot}}
/>
<Input
@value={{this.accountChallenge}}
id="new-account-challenge"
/>
</div>
</div>
{{#if this.requireInviteCode}}
<div class="input-group create-account__invite-code">
<Input
{{on "focusin" this.scrollInputIntoView}}
@value={{this.inviteCode}}
id="inviteCode"
class={{value-entered this.inviteCode}}
/>
<label class="alt-placeholder" for="invite-code">
{{i18n "user.invite_code.title"}}
</label>
<span class="more-info">
{{i18n "user.invite_code.instructions"}}
</span>
</div> </div>
{{/if}} {{/if}}
<div class="password-confirmation"> <PluginOutlet
<label for="new-account-password-confirmation"> @name="create-account-after-password"
{{i18n "user.password_confirmation.title"}} @outletArgs={{hash
</label> accountName=this.accountName
<HoneypotInput accountUsername=this.accountUsername
@id="new-account-confirmation" accountPassword=this.accountPassword
@autocomplete="new-password" userFields=this.userFields
@value={{this.accountHoneypot}} }}
/> />
<Input
@value={{this.accountChallenge}}
id="new-account-challenge"
/>
</div>
</div>
{{#if this.requireInviteCode}} {{#if (and this.showFullname (not this.fullnameRequired))}}
<div class="input-group create-account__invite-code"> <FullnameInput
<Input @nameValidation={{this.nameValidation}}
{{on "focusin" this.scrollInputIntoView}} @nameTitle={{this.nameTitle}}
@value={{this.inviteCode}} @accountName={{this.accountName}}
id="inviteCode" @nameDisabled={{this.nameDisabled}}
class={{value-entered this.inviteCode}} @onFocusIn={{this.scrollInputIntoView}}
class="input-group create-account__fullname"
/> />
<label class="alt-placeholder" for="invite-code"> {{/if}}
{{i18n "user.invite_code.title"}}
</label>
<span class="more-info">
{{i18n "user.invite_code.instructions"}}
</span>
</div>
{{/if}}
<PluginOutlet {{#if this.userFields}}
@name="create-account-after-password" <div class="user-fields">
@outletArgs={{hash {{#each this.userFields as |f|}}
accountName=this.accountName <div class="input-group">
accountUsername=this.accountUsername <UserField
accountPassword=this.accountPassword {{on "focusin" this.scrollInputIntoView}}
userFields=this.userFields @field={{f.field}}
}} @value={{f.value}}
/> @validation={{f.validation}}
class={{value-entered f.value}}
/>
</div>
{{/each}}
</div>
{{/if}}
{{#if (and this.showFullname (not this.fullnameRequired))}} <PluginOutlet
<FullnameInput @name="create-account-after-user-fields"
@nameValidation={{this.nameValidation}} @outletArgs={{hash
@nameTitle={{this.nameTitle}} accountName=this.accountName
@accountName={{this.accountName}} accountUsername=this.accountUsername
@nameDisabled={{this.nameDisabled}} accountPassword=this.accountPassword
@onFocusIn={{this.scrollInputIntoView}} userFields=this.userFields
class="input-group create-account__fullname" }}
/>
</form>
{{#if this.site.desktopView}}
<SignupPageCta
@formSubmitted={{this.formSubmitted}}
@hasAuthOptions={{this.hasAuthOptions}}
@createAccount={{this.createAccount}}
@submitDisabled={{this.submitDisabled}}
@disclaimerHtml={{this.disclaimerHtml}}
/> />
{{/if}} {{/if}}
{{#if this.userFields}}
<div class="user-fields">
{{#each this.userFields as |f|}}
<div class="input-group">
<UserField
{{on "focusin" this.scrollInputIntoView}}
@field={{f.field}}
@value={{f.value}}
@validation={{f.validation}}
class={{value-entered f.value}}
/>
</div>
{{/each}}
</div>
{{/if}}
<PluginOutlet
@name="create-account-after-user-fields"
@outletArgs={{hash
accountName=this.accountName
accountUsername=this.accountUsername
accountPassword=this.accountPassword
userFields=this.userFields
}}
/>
</form>
{{#if this.site.desktopView}}
<SignupPageCta
@formSubmitted={{this.formSubmitted}}
@hasAuthOptions={{this.hasAuthOptions}}
@createAccount={{this.createAccount}}
@submitDisabled={{this.submitDisabled}}
@disclaimerHtml={{this.disclaimerHtml}}
/>
{{/if}} {{/if}}
{{#if this.skipConfirmation}}
{{loading-spinner size="large"}}
{{/if}}
</div>
{{#if this.hasAtLeastOneLoginButton}}
{{#if this.site.mobileView}}
<div class="login-or-separator"><span>
{{i18n "login.or"}}</span></div>{{/if}}
<div class="login-right-side">
<LoginButtons
@externalLogin={{this.externalLogin}}
@context="create-account"
/>
</div>
{{/if}} {{/if}}
{{#if this.skipConfirmation}} {{#if (and this.showCreateForm this.site.mobileView)}}
{{loading-spinner size="large"}} <SignupPageCta
@formSubmitted={{this.formSubmitted}}
@hasAuthOptions={{this.hasAuthOptions}}
@createAccount={{this.createAccount}}
@submitDisabled={{this.submitDisabled}}
@disclaimerHtml={{this.disclaimerHtml}}
/>
{{/if}} {{/if}}
</div> </div>
{{#if this.hasAtLeastOneLoginButton}}
{{#if this.site.mobileView}}
<div class="login-or-separator"><span>
{{i18n "login.or"}}</span></div>{{/if}}
<div class="login-right-side">
<LoginButtons
@externalLogin={{this.externalLogin}}
@context="create-account"
/>
</div>
{{/if}}
{{#if (and this.showCreateForm this.site.mobileView)}}
<SignupPageCta
@formSubmitted={{this.formSubmitted}}
@hasAuthOptions={{this.hasAuthOptions}}
@createAccount={{this.createAccount}}
@submitDisabled={{this.submitDisabled}}
@disclaimerHtml={{this.disclaimerHtml}}
/>
{{/if}}
</div> </div>
</div> {{/if}}

View File

@ -232,6 +232,34 @@ shared_examples "social authentication scenarios" do |signup_page_object, login_
expect(page).to have_css(".header-dropdown-toggle.current-user") expect(page).to have_css(".header-dropdown-toggle.current-user")
end end
end end
context "when there is only one external login method enabled" do
before do
SiteSetting.enable_google_oauth2_logins = true
SiteSetting.enable_local_logins = false
end
after { reset_omniauth_config(:google_oauth2) }
it "automatically redirects to the external auth provider" do
mock_google_auth
visit("/login")
expect(signup_form).to be_open
expect(signup_form).to have_no_password_input
expect(signup_form).to have_valid_username
expect(signup_form).to have_valid_email
signup_form.click_create_account
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
it "automatically redirects to the external auth provider when skipping the signup form" do
SiteSetting.auth_skip_create_confirm = true
mock_google_auth
visit("/login")
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
end end
context "when user exists" do context "when user exists" do