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

This adds back the fixes from
https://github.com/discourse/discourse/pull/30928 that were reverted by
https://github.com/discourse/discourse/pull/30960.
This commit is contained in:
Jan Cernik 2025-02-07 12:40:45 -03:00 committed by GitHub
parent 891d8fe574
commit 42447770bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 678 additions and 453 deletions

View File

@ -119,7 +119,8 @@ export default class LoginPageController extends Controller {
get shouldTriggerRouteAction() {
return (
!this.siteSettings.full_page_login ||
this.siteSettings.enable_discourse_connect
this.siteSettings.enable_discourse_connect ||
this.singleExternalLogin
);
}
@ -220,7 +221,10 @@ export default class LoginPageController extends Controller {
const returnPath = encodeURIComponent(window.location.pathname);
window.location = getURL("/session/sso?return_path=" + returnPath);
} else {
if (this.isOnlyOneExternalLoginMethod) {
if (
this.isOnlyOneExternalLoginMethod &&
this.siteSettings.auth_immediately
) {
// we will automatically redirect to the external auth service
this.login.externalLogin(this.externalLoginMethods[0], {
signup: true,

View File

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

View File

@ -7,12 +7,26 @@ import DiscourseRoute from "discourse/routes/discourse";
export default class LoginRoute extends DiscourseRoute {
@service siteSettings;
@service router;
@service login;
beforeModel() {
if (
!this.siteSettings.login_required &&
(!this.siteSettings.full_page_login ||
this.siteSettings.enable_discourse_connect)
if (this.siteSettings.login_required) {
if (
this.login.isOnlyOneExternalLoginMethod &&
this.siteSettings.auth_immediately &&
!document.getElementById("data-authentication")?.dataset
.authenticationData
) {
this.login.singleExternalLogin();
}
} else if (
this.login.isOnlyOneExternalLoginMethod &&
this.siteSettings.full_page_login
) {
this.login.singleExternalLogin();
} else if (
!this.siteSettings.full_page_login ||
this.siteSettings.enable_discourse_connect
) {
this.router
.replaceWith(`/${defaultHomepage()}`)
@ -38,5 +52,13 @@ export default class LoginRoute extends DiscourseRoute {
if (this.siteSettings.login_required) {
controller.set("showLogin", false);
}
if (this.login.isOnlyOneExternalLoginMethod) {
if (this.siteSettings.auth_immediately) {
controller.set("isRedirectingToExternalAuth", true);
} else {
controller.set("singleExternalLogin", this.login.singleExternalLogin);
}
}
}
}

View File

@ -4,11 +4,30 @@ import { service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse";
export default class SignupRoute extends DiscourseRoute {
@service router;
@service siteSettings;
@service router;
@service login;
authComplete = false;
beforeModel() {
this.showCreateAccount();
this.authComplete = document.getElementById(
"data-authentication"
)?.dataset.authenticationData;
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("isRedirectingToExternalAuth", true);
}
}
@action

View File

@ -1,9 +1,12 @@
import { action } from "@ember/object";
import Service from "@ember/service";
import Service, { service } from "@ember/service";
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
import { findAll } from "discourse/models/login-method";
@disableImplicitInjections
export default class LoginService extends Service {
@service siteSettings;
@action
async externalLogin(
loginMethod,
@ -16,4 +19,20 @@ export default class LoginService extends Service {
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,187 @@
{{#if
(and
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}} />
{{hide-application-header-buttons "search" "login" "signup" "menu"}}
{{hide-application-sidebar}}
{{body-class "login-page"}}
<div class={{concat-class "login-body" this.bodyClasses}}>
<PluginOutlet @name="login-before-modal-body" @connectorTagName="div" />
{{#if this.isRedirectingToExternalAuth}}
{{! Hide the login form if the site has only one external }}
{{! authentication method and is being automatically redirected to it }}
{{loading-spinner}}
{{else}}
{{#if
(and
this.siteSettings.full_page_login
(or this.showLogin (not this.siteSettings.login_required))
)
}}
{{! Show the full page login form }}
<div class="login-fullpage">
<FlashMessage @flash={{this.flash}} @type={{this.flashType}} />
{{#if this.hasNoLoginOptions}}
<div class={{if this.site.desktopView "login-left-side"}}>
<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}}
<div class={{concat-class "login-body" this.bodyClasses}}>
<PluginOutlet @name="login-before-modal-body" @connectorTagName="div" />
{{#if this.canLoginLocal}}
{{#if this.hasNoLoginOptions}}
<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>
{{/if}}
{{#if (and this.showLoginButtons this.site.desktopView)}}
{{#unless this.canLoginLocal}}
<div class="login-left-side">
<WelcomeHeader @header={{i18n "login.header_title"}} />
<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>
{{/unless}}
{{#if this.hasAtLeastOneLoginButton}}
<div class="login-right-side">
</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}}
<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>
{{/if}}
{{/if}}
{{/if}}
{{#if this.site.mobileView}}
{{#unless this.hasNoLoginOptions}}
<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}}
/>
{{/unless}}
{{/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 (and this.showLoginButtons this.site.desktopView)}}
{{#unless this.canLoginLocal}}
<div class="login-left-side">
<WelcomeHeader @header={{i18n "login.header_title"}} />
</div>
{{/unless}}
{{#if this.hasAtLeastOneLoginButton}}
<div class="login-right-side">
<LoginButtons
@externalLogin={{this.externalLoginAction}}
@passkeyLogin={{this.passkeyLogin}}
@context="login"
/>
</div>
{{/if}}
{{/if}}
{{/if}}
<DButton
@action={{if
this.shouldTriggerRouteAction
(route-action "showLogin")
this.showFullPageLogin
}}
@icon="user"
@label="log_in"
class="btn-primary login-button"
/>
</div>
{{#if this.site.mobileView}}
{{#unless this.hasNoLoginOptions}}
<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}}
/>
{{/unless}}
{{/if}}
</div>
</div>
</section>
{{else}}
{{! Show the login-required splash screen }}
{{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}}

View File

@ -2,288 +2,298 @@
{{hide-application-header-buttons "search" "login" "signup" "menu"}}
{{hide-application-sidebar}}
{{body-class "signup-page"}}
<div class="signup-fullpage">
<FlashMessage @flash={{this.flash}} @type={{this.flashType}} />
<div class={{concat-class "signup-body" this.bodyClasses}}>
<PluginOutlet
@name="create-account-before-modal-body"
@connectorTagName="div"
/>
{{#if this.isRedirectingToExternalAuth}}
{{! Hide the signup form if the site has only one external }}
{{! authentication method and is being automatically redirected to it }}
{{loading-spinner}}
{{else}}
<div class="signup-fullpage">
<FlashMessage @flash={{this.flash}} @type={{this.flashType}} />
<div
class={{concat-class
(if this.site.desktopView "login-left-side")
this.authOptions.auth_provider
}}
>
<SignupProgressBar @step="signup" />
<WelcomeHeader
id="create-account-title"
@header={{i18n "create_account.header_title"}}
<div class={{concat-class "signup-body" this.bodyClasses}}>
<PluginOutlet
@name="create-account-before-modal-body"
@connectorTagName="div"
/>
<div
class={{concat-class
(if this.site.desktopView "login-left-side")
this.authOptions.auth_provider
}}
>
<PluginOutlet
@name="create-account-header-bottom"
@outletArgs={{hash showLogin=(route-action "showLogin")}}
/>
</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}}
{{on "input" this.setAccountUsername}}
type="text"
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}}
<SignupProgressBar @step="signup" />
<WelcomeHeader
id="create-account-title"
@header={{i18n "create_account.header_title"}}
>
<PluginOutlet
@name="create-account-before-password"
@outletArgs={{hash
accountName=this.accountName
accountUsername=this.accountUsername
accountPassword=this.accountPassword
userFields=this.userFields
authOptions=this.authOptions
}}
@name="create-account-header-bottom"
@outletArgs={{hash showLogin=(route-action "showLogin")}}
/>
<div class="input-group create-account__password">
{{#if this.passwordRequired}}
<PasswordField
{{on "focusout" this.togglePasswordValidation}}
</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}}
@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}}
@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-password">
{{i18n "user.password.title"}}
<label class="alt-placeholder" for="new-account-email">
{{i18n "user.email.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 class="more-info" id="password-validation-more-info">
{{this.passwordInstructions}}
</span>
{{#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}}
<div
class={{concat-class
"caps-lock-warning"
(unless this.capsLockOn "hidden")
</span>
{{/if}}
</div>
<div class="input-group create-account__username">
<input
{{on "focusin" this.scrollInputIntoView}}
{{on "input" this.setAccountUsername}}
type="text"
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
}}
>
{{d-icon "triangle-exclamation"}}
{{i18n "login.caps_lock_warning"}}
<span
class="more-info"
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>
{{/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>
{{/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>
<PluginOutlet
@name="create-account-after-password"
@outletArgs={{hash
accountName=this.accountName
accountUsername=this.accountUsername
accountPassword=this.accountPassword
userFields=this.userFields
}}
/>
{{#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}}
{{#if (and this.showFullname (not this.fullnameRequired))}}
<FullnameInput
@nameValidation={{this.nameValidation}}
@nameTitle={{this.nameTitle}}
@accountName={{this.accountName}}
@nameDisabled={{this.nameDisabled}}
@onFocusIn={{this.scrollInputIntoView}}
class="input-group create-account__fullname"
/>
<label class="alt-placeholder" for="invite-code">
{{i18n "user.invite_code.title"}}
</label>
<span class="more-info">
{{i18n "user.invite_code.instructions"}}
</span>
</div>
{{/if}}
{{/if}}
<PluginOutlet
@name="create-account-after-password"
@outletArgs={{hash
accountName=this.accountName
accountUsername=this.accountUsername
accountPassword=this.accountPassword
userFields=this.userFields
}}
/>
{{#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}}
{{#if (and this.showFullname (not this.fullnameRequired))}}
<FullnameInput
@nameValidation={{this.nameValidation}}
@nameTitle={{this.nameTitle}}
@accountName={{this.accountName}}
@nameDisabled={{this.nameDisabled}}
@onFocusIn={{this.scrollInputIntoView}}
class="input-group create-account__fullname"
<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 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 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 this.skipConfirmation}}
{{loading-spinner size="large"}}
{{#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>
{{#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>
{{/if}}

View File

@ -3,7 +3,7 @@ import { test } from "qunit";
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
acceptance("Login Required", function (needs) {
needs.settings({ login_required: true });
needs.settings({ login_required: true, full_page_login: false });
test("redirect", async function (assert) {
await visit("/latest");
@ -13,17 +13,33 @@ acceptance("Login Required", function (needs) {
"it redirects them to login"
);
await click("#site-logo");
assert.strictEqual(
currentRouteName(),
"login",
"clicking the logo keeps them on login"
);
await click("header .login-button");
assert.dom(".login-modal").exists("they can still access the login modal");
await click(".login-button");
assert.dom(".login-modal").exists("login modal is shown");
await click(".d-modal__header .modal-close");
assert.dom(".login-modal").doesNotExist("closes the login modal");
});
});
acceptance("Login Required - Full page login", function (needs) {
needs.settings({ login_required: true, full_page_login: true });
test("page", async function (assert) {
await visit("/");
assert.strictEqual(
currentRouteName(),
"login",
"it redirects them to login"
);
await click(".login-button");
assert.dom(".login-left-side").exists("login form is shown");
assert
.dom(".login-welcome")
.doesNotExist("login welcome is no longer shown");
await click(".logo-big");
assert.dom(".login-left-side").doesNotExist("closes the login modal");
assert.dom(".login-welcome").exists("login welcome is shown");
});
});

View File

@ -600,6 +600,7 @@ login:
client: true
auth_immediately:
default: true
client: true
auth_overrides_email:
default: false
validator: "SsoOverridesEmailValidator"

View File

@ -232,6 +232,143 @@ shared_examples "social authentication scenarios" do |signup_page_object, login_
expect(page).to have_css(".header-dropdown-toggle.current-user")
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) }
context "when login is required" do
before { SiteSetting.login_required = true }
it "automatically redirects when auth_immediately is enabled" do
SiteSetting.auth_immediately = true
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
visit("/signup")
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
visit("/")
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 "shows the login-required page when auth_immediately is disabled" do
SiteSetting.auth_immediately = false
mock_google_auth
visit("/login")
expect(page).to have_css(".login-welcome")
expect(page).to have_css(".site-logo")
visit("/")
expect(page).to have_css(".login-welcome")
expect(page).to have_css(".site-logo")
find(".login-welcome .login-button").click
expect(signup_form).to be_open
visit("/")
find(".login-welcome .sign-up-button").click
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 when going to /signup" do
SiteSetting.auth_immediately = false
mock_google_auth
visit("/signup")
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 when skipping the signup form" do
SiteSetting.auth_skip_create_confirm = true
SiteSetting.auth_immediately = true
mock_google_auth
visit("/login")
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
it "works with existing users when auth_immediately is enabled" do
SiteSetting.auth_immediately = false
SiteSetting.login_required = true
mock_google_auth
visit("/signup")
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
find(".header-dropdown-toggle.current-user").click
find("#user-menu-button-profile").click
find("#quick-access-profile .logout").click
visit("/")
find(".login-welcome .login-button").click
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
it "automatically redirects when using the login button" do
SiteSetting.auth_immediately = false
mock_google_auth
visit("/")
find(".header-buttons .login-button").click
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 when using the routes" do
SiteSetting.auth_immediately = false
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
visit("/signup")
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
end
end
context "when user exists" do