DEV: Show login-required screen in root route (#32350)

This changes means that login-required sites will show a splash screen
on `/`. Users heading to `/login` or `/signup` will see the respective
forms.
This commit is contained in:
Penar Musaraj
2025-04-30 12:32:18 -04:00
committed by GitHub
parent 56edbcd9e2
commit 10df7fdee0
18 changed files with 233 additions and 217 deletions
@@ -0,0 +1,5 @@
import Controller, { inject as controller } from "@ember/controller";
export default class LoginRequiredController extends Controller {
@controller application;
}
@@ -35,6 +35,7 @@ function rewriteIfNeeded(url, transition) {
const intentUrl = transition?.intent?.url;
if (
intentUrl?.startsWith(homepageDestination()) ||
intentUrl?.startsWith("/login-required") ||
(transition?.intent.name === `discovery.${defaultHomepage()}` &&
transition?.intent.queryParams[homepageRewriteParam])
) {
@@ -68,6 +68,7 @@ export default function () {
this.route("category", { path: "/c/*category_slug_path_with_id" });
this.route("custom");
this.route("login-required");
});
this.route("groups", { resetNamespace: true, path: "/g" }, function () {
@@ -271,9 +271,6 @@ export default class ApplicationRoute extends DiscourseRoute {
} else {
this.router.transitionTo("login").then((login) => {
login.controller.set("canSignUp", this.controller.canSignUp);
if (this.siteSettings.login_required) {
login.controller.set("showLogin", true);
}
});
}
}
@@ -36,13 +36,6 @@ export default class DiscourseRoute extends Route {
once(this, this._refreshTitleOnce);
}
redirectIfLoginRequired() {
const app = this.controllerFor("application");
if (app.get("loginRequired")) {
this.router.replaceWith("login");
}
}
isCurrentUser(user) {
if (!this.currentUser) {
return false; // the current user is anonymous
@@ -0,0 +1,8 @@
import StaticPage from "discourse/models/static-page";
import DiscourseRoute from "discourse/routes/discourse";
export default class LoginRequiredRoute extends DiscourseRoute {
model() {
return StaticPage.find("login");
}
}
@@ -11,13 +11,17 @@ export default class DiscoveryRoute extends DiscourseRoute {
@service router;
@service session;
@service site;
@service siteSettings;
queryParams = {
filter: { refreshModel: true },
};
redirect() {
return this.redirectIfLoginRequired();
if (this.siteSettings.login_required && !this.currentUser) {
this.router.transitionTo("/login-required?_discourse_homepage_rewrite=1");
return;
}
}
beforeModel(transition) {
@@ -58,10 +58,6 @@ 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);
@@ -48,10 +48,6 @@ export default class TopicRoute extends DiscourseRoute {
};
}
redirect() {
return this.redirectIfLoginRequired();
}
titleToken() {
const model = this.modelFor("topic");
if (model) {
@@ -0,0 +1,62 @@
import { hash } from "@ember/helper";
import { htmlSafe } from "@ember/template";
import RouteTemplate from "ember-route-template";
import DButton from "discourse/components/d-button";
import PluginOutlet from "discourse/components/plugin-outlet";
import bodyClass from "discourse/helpers/body-class";
import hideApplicationHeaderButtons from "discourse/helpers/hide-application-header-buttons";
import hideApplicationSidebar from "discourse/helpers/hide-application-sidebar";
import routeAction from "discourse/helpers/route-action";
export default RouteTemplate(
<template>
{{hideApplicationHeaderButtons "search" "login" "signup" "menu"}}
{{hideApplicationSidebar}}
{{bodyClass "login-page"}}
{{bodyClass "static-login"}}
<section class="container">
<div class="contents clearfix body-page">
<div class="login-welcome">
<PluginOutlet
@name="above-login"
@outletArgs={{hash model=@controller.model}}
/>
<PluginOutlet @name="above-static" />
<div class="login-content">
{{htmlSafe @controller.model.html}}
</div>
<PluginOutlet @name="below-static" />
<PluginOutlet
@name="below-login"
@outletArgs={{hash model=@controller.model}}
/>
<div class="body-page-button-container">
{{#if @controller.application.canSignUp}}
<DButton
@action={{routeAction "showCreateAccount"}}
@label="sign_up"
class="btn-primary sign-up-button"
/>
{{/if}}
<DButton
@action={{routeAction "showLogin"}}
@icon="user"
@label="log_in"
class="btn-primary login-button"
/>
</div>
<PluginOutlet
@name="below-login-buttons"
@outletArgs={{hash model=@controller.model}}
/>
</div>
</div>
</section>
</template>
);
@@ -1,8 +1,7 @@
import { hash } from "@ember/helper";
import { htmlSafe } from "@ember/template";
import RouteTemplate from "ember-route-template";
import { and, not, or } from "truth-helpers";
import DButton from "discourse/components/d-button";
import { and } from "truth-helpers";
import FlashMessage from "discourse/components/flash-message";
import LocalLoginForm from "discourse/components/local-login-form";
import LoginButtons from "discourse/components/login-buttons";
@@ -14,7 +13,6 @@ import concatClass from "discourse/helpers/concat-class";
import hideApplicationHeaderButtons from "discourse/helpers/hide-application-header-buttons";
import hideApplicationSidebar from "discourse/helpers/hide-application-sidebar";
import loadingSpinner from "discourse/helpers/loading-spinner";
import routeAction from "discourse/helpers/route-action";
import { i18n } from "discourse-i18n";
export default RouteTemplate(
@@ -28,201 +26,143 @@ export default RouteTemplate(
{{! authentication method and is being automatically redirected to it }}
{{loadingSpinner}}
{{else}}
{{#if
(or @controller.showLogin (not @controller.siteSettings.login_required))
}}
{{! Show the full page login form }}
<div class="login-fullpage">
<FlashMessage
@flash={{@controller.flash}}
@type={{@controller.flashType}}
<div class="login-fullpage">
<FlashMessage
@flash={{@controller.flash}}
@type={{@controller.flashType}}
/>
<div class={{concatClass "login-body" @controller.bodyClasses}}>
<PluginOutlet
@name="login-before-modal-body"
@connectorTagName="div"
@outletArgs={{hash
flashChanged=this.flashChanged
flashTypeChanged=this.flashTypeChanged
}}
/>
<div class={{concatClass "login-body" @controller.bodyClasses}}>
<PluginOutlet
@name="login-before-modal-body"
@connectorTagName="div"
@outletArgs={{hash
flashChanged=this.flashChanged
flashTypeChanged=this.flashTypeChanged
}}
/>
{{#if @controller.hasNoLoginOptions}}
<div class={{if @controller.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">
{{htmlSafe
(i18n
"login.no_login_methods.description"
(hash adminLoginPath=@controller.adminLoginPath)
)
}}
</p>
</div>
{{#if @controller.hasNoLoginOptions}}
<div class={{if @controller.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">
{{htmlSafe
(i18n
"login.no_login_methods.description"
(hash adminLoginPath=@controller.adminLoginPath)
)
}}
</p>
</div>
{{else}}
{{#if @controller.site.mobileView}}
<WelcomeHeader @header={{i18n "login.header_title"}}>
<PluginOutlet
@name="login-header-bottom"
@outletArgs={{hash createAccount=@controller.createAccount}}
</div>
{{else}}
{{#if @controller.site.mobileView}}
<WelcomeHeader @header={{i18n "login.header_title"}}>
<PluginOutlet
@name="login-header-bottom"
@outletArgs={{hash createAccount=@controller.createAccount}}
/>
</WelcomeHeader>
{{#if @controller.showLoginButtons}}
<LoginButtons
@externalLogin={{@controller.externalLoginAction}}
@passkeyLogin={{@controller.passkeyLogin}}
@context="login"
/>
{{/if}}
{{/if}}
{{#if @controller.canLoginLocal}}
<div class={{if @controller.site.desktopView "login-left-side"}}>
{{#if @controller.site.desktopView}}
<WelcomeHeader @header={{i18n "login.header_title"}}>
<PluginOutlet
@name="login-header-bottom"
@outletArgs={{hash
createAccount=@controller.createAccount
}}
/>
</WelcomeHeader>
{{/if}}
<LocalLoginForm
@loginName={{@controller.loginName}}
@loginNameChanged={{@controller.loginNameChanged}}
@canLoginLocalWithEmail={{@controller.canLoginLocalWithEmail}}
@canUsePasskeys={{@controller.canUsePasskeys}}
@passkeyLogin={{@controller.passkeyLogin}}
@loginPassword={{@controller.loginPassword}}
@secondFactorMethod={{@controller.secondFactorMethod}}
@secondFactorToken={{@controller.secondFactorToken}}
@backupEnabled={{@controller.backupEnabled}}
@totpEnabled={{@controller.totpEnabled}}
@securityKeyAllowedCredentialIds={{@controller.securityKeyAllowedCredentialIds}}
@securityKeyChallenge={{@controller.securityKeyChallenge}}
@showSecurityKey={{@controller.showSecurityKey}}
@otherMethodAllowed={{@controller.otherMethodAllowed}}
@showSecondFactor={{@controller.showSecondFactor}}
@handleForgotPassword={{@controller.handleForgotPassword}}
@login={{@controller.triggerLogin}}
@flashChanged={{@controller.flashChanged}}
@flashTypeChanged={{@controller.flashTypeChanged}}
@securityKeyCredentialChanged={{@controller.securityKeyCredentialChanged}}
/>
{{#if @controller.site.desktopView}}
<LoginPageCta
@canLoginLocal={{@controller.canLoginLocal}}
@showSecurityKey={{@controller.showSecurityKey}}
@login={{@controller.triggerLogin}}
@loginButtonLabel={{@controller.loginButtonLabel}}
@loginDisabled={{@controller.loginDisabled}}
@showSignupLink={{@controller.showSignupLink}}
@createAccount={{@controller.createAccount}}
@loggingIn={{@controller.loggingIn}}
@showSecondFactor={{@controller.showSecondFactor}}
/>
</WelcomeHeader>
{{#if @controller.showLoginButtons}}
{{/if}}
</div>
{{/if}}
{{#if
(and @controller.showLoginButtons @controller.site.desktopView)
}}
{{#unless @controller.canLoginLocal}}
<div class="login-left-side">
<WelcomeHeader @header={{i18n "login.header_title"}} />
</div>
{{/unless}}
{{#if @controller.hasAtLeastOneLoginButton}}
<div class="login-right-side">
<LoginButtons
@externalLogin={{@controller.externalLoginAction}}
@passkeyLogin={{@controller.passkeyLogin}}
@context="login"
/>
{{/if}}
{{/if}}
{{#if @controller.canLoginLocal}}
<div
class={{if @controller.site.desktopView "login-left-side"}}
>
{{#if @controller.site.desktopView}}
<WelcomeHeader @header={{i18n "login.header_title"}}>
<PluginOutlet
@name="login-header-bottom"
@outletArgs={{hash
createAccount=@controller.createAccount
}}
/>
</WelcomeHeader>
{{/if}}
<LocalLoginForm
@loginName={{@controller.loginName}}
@loginNameChanged={{@controller.loginNameChanged}}
@canLoginLocalWithEmail={{@controller.canLoginLocalWithEmail}}
@canUsePasskeys={{@controller.canUsePasskeys}}
@passkeyLogin={{@controller.passkeyLogin}}
@loginPassword={{@controller.loginPassword}}
@secondFactorMethod={{@controller.secondFactorMethod}}
@secondFactorToken={{@controller.secondFactorToken}}
@backupEnabled={{@controller.backupEnabled}}
@totpEnabled={{@controller.totpEnabled}}
@securityKeyAllowedCredentialIds={{@controller.securityKeyAllowedCredentialIds}}
@securityKeyChallenge={{@controller.securityKeyChallenge}}
@showSecurityKey={{@controller.showSecurityKey}}
@otherMethodAllowed={{@controller.otherMethodAllowed}}
@showSecondFactor={{@controller.showSecondFactor}}
@handleForgotPassword={{@controller.handleForgotPassword}}
@login={{@controller.triggerLogin}}
@flashChanged={{@controller.flashChanged}}
@flashTypeChanged={{@controller.flashTypeChanged}}
@securityKeyCredentialChanged={{@controller.securityKeyCredentialChanged}}
/>
{{#if @controller.site.desktopView}}
<LoginPageCta
@canLoginLocal={{@controller.canLoginLocal}}
@showSecurityKey={{@controller.showSecurityKey}}
@login={{@controller.triggerLogin}}
@loginButtonLabel={{@controller.loginButtonLabel}}
@loginDisabled={{@controller.loginDisabled}}
@showSignupLink={{@controller.showSignupLink}}
@createAccount={{@controller.createAccount}}
@loggingIn={{@controller.loggingIn}}
@showSecondFactor={{@controller.showSecondFactor}}
/>
{{/if}}
</div>
{{/if}}
{{#if
(and @controller.showLoginButtons @controller.site.desktopView)
}}
{{#unless @controller.canLoginLocal}}
<div class="login-left-side">
<WelcomeHeader @header={{i18n "login.header_title"}} />
</div>
{{/unless}}
{{#if @controller.hasAtLeastOneLoginButton}}
<div class="login-right-side">
<LoginButtons
@externalLogin={{@controller.externalLoginAction}}
@passkeyLogin={{@controller.passkeyLogin}}
@context="login"
/>
</div>
{{/if}}
{{/if}}
{{/if}}
{{/if}}
{{#if @controller.site.mobileView}}
{{#unless @controller.hasNoLoginOptions}}
<LoginPageCta
@canLoginLocal={{@controller.canLoginLocal}}
@showSecurityKey={{@controller.showSecurityKey}}
@login={{@controller.triggerLogin}}
@loginButtonLabel={{@controller.loginButtonLabel}}
@loginDisabled={{@controller.loginDisabled}}
@showSignupLink={{@controller.showSignupLink}}
@createAccount={{@controller.createAccount}}
@loggingIn={{@controller.loggingIn}}
@showSecondFactor={{@controller.showSecondFactor}}
/>
{{/unless}}
{{/if}}
</div>
{{#if @controller.site.mobileView}}
{{#unless @controller.hasNoLoginOptions}}
<LoginPageCta
@canLoginLocal={{@controller.canLoginLocal}}
@showSecurityKey={{@controller.showSecurityKey}}
@login={{@controller.triggerLogin}}
@loginButtonLabel={{@controller.loginButtonLabel}}
@loginDisabled={{@controller.loginDisabled}}
@showSignupLink={{@controller.showSignupLink}}
@createAccount={{@controller.createAccount}}
@loggingIn={{@controller.loggingIn}}
@showSecondFactor={{@controller.showSecondFactor}}
/>
{{/unless}}
{{/if}}
</div>
{{else}}
{{! Show the login-required splash screen }}
{{bodyClass "static-login"}}
<section class="container">
<div class="contents clearfix body-page">
<div class="login-welcome">
<PluginOutlet
@name="above-login"
@outletArgs={{hash model=@controller.model}}
/>
<PluginOutlet @name="above-static" />
<div class="login-content">
{{htmlSafe @controller.model.html}}
</div>
<PluginOutlet @name="below-static" />
<PluginOutlet
@name="below-login"
@outletArgs={{hash model=@controller.model}}
/>
<div class="body-page-button-container">
{{#if @controller.application.canSignUp}}
<DButton
@action={{routeAction "showCreateAccount"}}
@label="sign_up"
class="btn-primary sign-up-button"
/>
{{/if}}
<DButton
@action={{if
@controller.shouldTriggerRouteAction
(routeAction "showLogin")
@controller.showFullPageLogin
}}
@icon="user"
@label="log_in"
class="btn-primary login-button"
/>
</div>
<PluginOutlet
@name="below-login-buttons"
@outletArgs={{hash model=@controller.model}}
/>
</div>
</div>
</section>
{{/if}}
</div>
{{/if}}
</template>
);
@@ -9,8 +9,8 @@ acceptance("Login Required - Full page login", function (needs) {
await visit("/");
assert.strictEqual(
currentRouteName(),
"login",
"it redirects them to login"
"discovery.login-required",
"it shows the login required splash"
);
await click(".login-button");
@@ -43,11 +43,19 @@ acceptance("Static pages", function () {
test("Login-required page", async function (assert) {
this.siteSettings.login_required = true;
await visit("/login");
await visit("/");
assert.strictEqual(currentRouteName(), "login");
assert.strictEqual(currentRouteName(), "discovery.login-required");
assert.dom(".body-page").exists("The content is present");
assert.dom(".sign-up-button").exists();
assert.dom(".login-button").exists();
});
test("Login-required - Login Route", async function (assert) {
this.siteSettings.login_required = true;
await visit("/login");
assert.strictEqual(currentRouteName(), "login");
assert.dom(".login-fullpage").exists("The login full page form is shown");
});
});
@@ -753,6 +753,7 @@ class ApplicationController < ActionController::Base
cookies[:destination_url] = destination_url
redirect_to path("/auth/#{Discourse.enabled_authenticators.first.name}")
else
return if request.path == path("/") && !cookies[:authentication_data]
# save original URL in a cookie (javascript redirects after login in this case)
cookies[:destination_url] = destination_url
redirect_to path("/login")
+1
View File
@@ -548,6 +548,7 @@ Discourse::Application.routes.draw do
post "login" => "static#enter"
get "login" => "static#show", :id => "login"
get "login-required" => "static#show", :id => "login"
get "login-preferences" => "static#show", :id => "login"
get "signup" => "static#show", :id => "signup"
get "password-reset" => "static#show", :id => "password_reset"
+12 -7
View File
@@ -14,9 +14,10 @@ RSpec.describe ApplicationController do
expect(response.headers["Cache-Control"]).to eq("no-cache, no-store")
end
it "should redirect to login normally" do
it "should not redirect to login" do
get "/"
expect(response).to redirect_to("/login")
expect(response).not_to redirect_to("/login")
expect(response.status).to eq(200)
end
it "should redirect to SSO if enabled" do
@@ -27,10 +28,11 @@ RSpec.describe ApplicationController do
end
it "should redirect to authenticator if only one, and local logins disabled" do
# Local logins and google enabled, direct to login UI
# Local logins and google enabled, show login UI
SiteSetting.enable_google_oauth2_logins = true
get "/"
expect(response).to redirect_to("/login")
expect(response).not_to redirect_to("/login")
expect(response.status).to eq(200)
# Only google enabled, login immediately
SiteSetting.enable_local_logins = false
@@ -40,7 +42,8 @@ RSpec.describe ApplicationController do
# Google and GitHub enabled, direct to login UI
SiteSetting.enable_github_logins = true
get "/"
expect(response).to redirect_to("/login")
expect(response).not_to redirect_to("/login")
expect(response.status).to eq(200)
end
it "should not redirect to SSO when auth_immediately is disabled" do
@@ -49,7 +52,8 @@ RSpec.describe ApplicationController do
SiteSetting.enable_discourse_connect = true
get "/"
expect(response).to redirect_to("/login")
expect(response).not_to redirect_to("/login")
expect(response.status).to eq(200)
end
it "should not redirect to authenticator when auth_immediately is disabled" do
@@ -58,7 +62,8 @@ RSpec.describe ApplicationController do
SiteSetting.enable_local_logins = false
get "/"
expect(response).to redirect_to("/login")
expect(response).not_to redirect_to("/login")
expect(response.status).to eq(200)
end
context "with omniauth in test mode" do
-1
View File
@@ -134,7 +134,6 @@ shared_examples "login scenarios" do |login_page_object|
Fabricate(:group_private_message_topic, user: user, recipient_group: group)
visit "/t/#{pm.id}"
find(".login-welcome .login-button").click
login_form.fill(username: "john", password: "supersecurepassword").click_login
expect(page).to have_css(".header-dropdown-toggle.current-user")
+1 -2
View File
@@ -272,8 +272,7 @@ shared_examples "social authentication scenarios" do |signup_page_object, login_
mock_google_auth
visit("/login")
expect(page).to have_css(".login-welcome")
expect(page).to have_css(".site-logo")
expect(page).to have_css(".btn-social")
visit("/")
expect(page).to have_css(".login-welcome")