2
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2025-08-17 18:04:11 +08:00

FIX: 'destination_url' cookie handling (#33072)

Since the introduction of dedicated login and signup pages (as opposed
to modals), we've been seeing reports of issues where visitors aren't
redirected back to the "page" they were at when they initiated the
_authentication_ process.

Since we have a bazillion of ways a user might authenticate
(credentials, social logins, SSO, passkeys, discourse connect, etc...),
it's really hard to know what a change will impact.

The goal of this PR is to "simplify" the way we handle this "redirection
back to origin" by leveraging the use of a single `destination_url`
cookie set on the client-side.

The changes remove scattered cookie-setting code and consolidate the redirection logic to ensure users are properly redirected back to their original page after authentication.

- Centralized destination URL cookie management in routes and authentication flows
- Removed manual cookie setting from various components in favor of automatic handling
- Updated test scenarios to properly test the new redirection behavior
This commit is contained in:
Régis Hanol 2025-08-06 10:09:01 +02:00 committed by GitHub
parent fea4d73787
commit ced043be3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 759 additions and 889 deletions

View file

@ -5,7 +5,6 @@ import { service } from "@ember/service";
import { classNames } from "@ember-decorators/component";
import DButton from "discourse/components/d-button";
import { popupAjaxError } from "discourse/lib/ajax-error";
import cookie from "discourse/lib/cookie";
import discourseComputed from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
import RequestGroupMembershipForm from "./modal/request-group-membership-form";
@ -37,11 +36,6 @@ export default class GroupMembershipButton extends Component {
return !!isGroupUser;
}
_showLoginModal() {
this.showLogin();
cookie("destination_url", window.location.href);
}
removeFromGroup() {
const model = this.model;
model
@ -56,23 +50,23 @@ export default class GroupMembershipButton extends Component {
@action
joinGroup() {
if (this.currentUser) {
this.set("updatingMembership", true);
const group = this.model;
group
.join()
.then(() => {
group.set("is_group_user", true);
this.appEvents.trigger("group:join", group);
})
.catch(popupAjaxError)
.finally(() => {
this.set("updatingMembership", false);
});
} else {
this._showLoginModal();
if (!this.currentUser) {
return this.showLogin();
}
this.set("updatingMembership", true);
const group = this.model;
group
.join()
.then(() => {
group.set("is_group_user", true);
this.appEvents.trigger("group:join", group);
})
.catch(popupAjaxError)
.finally(() => {
this.set("updatingMembership", false);
});
}
@action
@ -92,15 +86,15 @@ export default class GroupMembershipButton extends Component {
@action
showRequestMembershipForm() {
if (this.currentUser) {
this.modal.show(RequestGroupMembershipForm, {
model: {
group: this.model,
},
});
} else {
this._showLoginModal();
if (!this.currentUser) {
return this.showLogin();
}
this.modal.show(RequestGroupMembershipForm, {
model: {
group: this.model,
},
});
}
<template>

View file

@ -4,14 +4,13 @@ import { action } from "@ember/object";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { isEmpty } from "@ember/utils";
import ForgotPassword from "discourse/components/modal/forgot-password";
import NotActivatedModal from "discourse/components/modal/not-activated";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { setting } from "discourse/lib/computed";
import cookie, { removeCookie } from "discourse/lib/cookie";
import escape from "discourse/lib/escape";
import getURL from "discourse/lib/get-url";
import { wantsNewWindow } from "discourse/lib/intercept-click";
import { areCookiesEnabled } from "discourse/lib/utilities";
import {
getPasskeyCredential,
@ -39,9 +38,6 @@ export default class LoginPageController extends Controller {
@tracked showSecondFactor = false;
@tracked loginPassword = "";
@tracked loginName = "";
@tracked canLoginLocal = this.siteSettings.enable_local_logins;
@tracked
canLoginLocalWithEmail = this.siteSettings.enable_local_logins_via_email;
@tracked secondFactorMethod = SECOND_FACTOR_METHODS.TOTP;
@tracked securityKeyCredential;
@tracked otherMethodAllowed;
@ -55,6 +51,9 @@ export default class LoginPageController extends Controller {
@tracked flash;
@tracked flashType;
@setting("enable_local_logins") canLoginLocal;
@setting("enable_local_logins_via_email") canLoginLocalWithEmail;
get isAwaitingApproval() {
return (
this.awaitingApproval &&
@ -109,24 +108,13 @@ export default class LoginPageController extends Controller {
}
get showSignupLink() {
return this.canSignUp && !this.showSecondFactor;
return this.application.canSignUp && !this.showSecondFactor;
}
get adminLoginPath() {
return getURL("/u/admin-login");
}
get shouldTriggerRouteAction() {
return (
this.siteSettings.enable_discourse_connect || this.singleExternalLogin
);
}
@action
showFullPageLogin() {
this.showLogin = true;
}
@action
async passkeyLogin(mediation = "optional") {
try {
@ -153,8 +141,6 @@ export default class LoginPageController extends Controller {
if (destinationUrl) {
removeCookie("destination_url");
window.location.assign(destinationUrl);
} else if (this.referrerTopicUrl) {
window.location.assign(this.referrerTopicUrl);
} else {
window.location.reload();
}
@ -167,21 +153,6 @@ export default class LoginPageController extends Controller {
}
}
@action
preloadLogin() {
const prefillUsername = document.querySelector(
"#hidden-login-form input[name=username]"
)?.value;
if (prefillUsername) {
this.loginName = prefillUsername;
this.loginPassword = document.querySelector(
"#hidden-login-form input[name=password]"
).value;
} else if (cookie("email")) {
this.loginName = cookie("email");
}
}
@action
securityKeyCredentialChanged(value) {
this.securityKeyCredential = value;
@ -207,45 +178,13 @@ export default class LoginPageController extends Controller {
this.loginPassword = event.target.value;
}
@action
showCreateAccount(createAccountProps = {}) {
if (this.site.isReadOnly) {
this.dialog.alert(i18n("read_only_mode.login_disabled"));
} else {
this.handleShowCreateAccount(createAccountProps);
}
}
handleShowCreateAccount(createAccountProps) {
if (this.siteSettings.enable_discourse_connect) {
const returnPath = encodeURIComponent(window.location.pathname);
window.location = getURL("/session/sso?return_path=" + returnPath);
} else {
if (
this.isOnlyOneExternalLoginMethod &&
this.siteSettings.auth_immediately
) {
// we will automatically redirect to the external auth service
this.login.externalLogin(this.externalLoginMethods[0], {
signup: true,
});
} else {
this.router.transitionTo("signup").then((signup) => {
Object.keys(createAccountProps || {}).forEach((key) => {
signup.controller.set(key, createAccountProps[key]);
});
});
}
}
}
@action
showNotActivated(props) {
this.modal.show(NotActivatedModal, { model: props });
}
@action
async triggerLogin() {
async localLogin() {
if (this.loginDisabled) {
return;
}
@ -268,9 +207,10 @@ export default class LoginPageController extends Controller {
timezone: moment.tz.guess(),
},
});
if (result && result.error) {
if (result?.error) {
this.loggingIn = false;
this.flash = null;
this.flashType = "error";
if (
(result.security_key_enabled || result.totp_enabled) &&
@ -304,111 +244,79 @@ export default class LoginPageController extends Controller {
reset_url: getURL("/password-reset"),
})
);
this.flashType = "error";
} else {
this.flash = result.error;
this.flashType = "error";
}
} else {
this.loggedIn = true;
// Trigger the browser's password manager using the hidden static login form:
const hiddenLoginForm = document.getElementById("hidden-login-form");
const applyHiddenFormInputValue = (value, key) => {
if (!hiddenLoginForm) {
return;
const _form = document.getElementById("hidden-login-form");
if (_form) {
const set = (key, value) => {
_form.querySelector(`input[name=${key}]`).value = value;
};
set("username", this.loginName);
set("password", this.loginPassword);
const destinationUrl = cookie("destination_url");
if (destinationUrl) {
removeCookie("destination_url");
set("redirect", destinationUrl);
} else {
set("redirect", window.location.href);
}
hiddenLoginForm.querySelector(`input[name=${key}]`).value = value;
};
applyHiddenFormInputValue(this.loginName, "username");
applyHiddenFormInputValue(this.loginPassword, "password");
const destinationUrl = cookie("destination_url");
if (destinationUrl) {
removeCookie("destination_url");
applyHiddenFormInputValue(destinationUrl, "redirect");
} else if (this.referrerTopicUrl) {
applyHiddenFormInputValue(this.referrerTopicUrl, "redirect");
} else {
applyHiddenFormInputValue(window.location.href, "redirect");
}
if (hiddenLoginForm) {
if (
navigator.userAgent.match(/(iPad|iPhone|iPod)/g) &&
navigator.userAgent.match(/Safari/g)
) {
if (this.capabilities.isIOS && this.capabilities.isSafari) {
// In case of Safari on iOS do not submit hidden login form
window.location.href = hiddenLoginForm.querySelector(
window.location.href = _form.querySelector(
"input[name=redirect]"
).value;
} else {
hiddenLoginForm.submit();
_form.submit();
}
}
return;
}
} catch (e) {
// Failed to login
if (e.jqXHR && e.jqXHR.status === 429) {
this.loggingIn = false;
this.flashType = "error";
if (e.jqXHR?.status === 429) {
this.flash = i18n("login.rate_limit");
this.flashType = "error";
} else if (
e.jqXHR &&
e.jqXHR.status === 503 &&
e.jqXHR.responseJSON.error_type === "read_only"
e.jqXHR?.status === 503 &&
e.jqXHR?.responseJSON?.error_type === "read_only"
) {
this.flash = i18n("read_only_mode.login_disabled");
this.flashType = "error";
} else if (!areCookiesEnabled()) {
this.flash = i18n("login.cookies_error");
this.flashType = "error";
} else {
this.flash = i18n("login.error");
this.flashType = "error";
}
this.loggingIn = false;
}
}
@action
externalLoginAction(loginMethod) {
if (this.loginDisabled) {
return;
externalLogin(loginMethod) {
if (!this.loginDisabled) {
this.login.externalLogin(loginMethod, {
setLoggingIn: (value) => (this.loggingIn = value),
});
}
this.login.externalLogin(loginMethod, {
signup: false,
setLoggingIn: (value) => (this.loggingIn = value),
});
}
@action
createAccount() {
let createAccountProps = {};
if (this.loginName && this.loginName.indexOf("@") > 0) {
createAccountProps.accountEmail = this.loginName;
createAccountProps.accountUsername = null;
// This makes the UX a little bit nicer by auto-filling the email/username when switching from /login to /signup
if (this.loginName?.indexOf("@") > 0) {
this.send("showCreateAccount", {
accountEmail: this.loginName,
accountUsername: "",
});
} else {
createAccountProps.accountUsername = this.loginName;
createAccountProps.accountEmail = null;
}
this.showCreateAccount(createAccountProps);
}
@action
interceptResetLink(event) {
if (
!wantsNewWindow(event) &&
event.target.href &&
new URL(event.target.href).pathname === getURL("/password-reset")
) {
event.preventDefault();
event.stopPropagation();
this.modal.show(ForgotPassword, {
model: {
emailOrUsername: this.loginName,
},
this.send("showCreateAccount", {
accountEmail: "",
accountUsername: this.loginName,
});
}
}

View file

@ -35,6 +35,7 @@ export default class SignupPageController extends Controller {
@tracked isDeveloper = false;
@tracked authOptions;
@tracked skipConfirmation;
accountChallenge = 0;
accountHoneypot = 0;
formSubmitted = false;
@ -450,12 +451,6 @@ export default class SignupPageController extends Controller {
accountPasswordConfirm: this.accountHoneypot,
};
const destinationUrl = this.authOptions?.destination_url;
if (!isEmpty(destinationUrl)) {
cookie("destination_url", destinationUrl, { path: "/" });
}
// Add the userFields to the data
if (!isEmpty(this.userFields)) {
attrs.userFields = {};
@ -475,30 +470,32 @@ export default class SignupPageController extends Controller {
this._challengeExpiry = 1;
// Trigger the browser's password manager using the hidden static login form:
const hiddenLoginForm = document.querySelector("#hidden-login-form");
if (hiddenLoginForm) {
hiddenLoginForm.querySelector("input[name=username]").value =
attrs.accountUsername;
hiddenLoginForm.querySelector("input[name=password]").value =
attrs.accountPassword;
hiddenLoginForm.querySelector("input[name=redirect]").value =
userPath("account-created");
hiddenLoginForm.submit();
const _form = document.getElementById("hidden-login-form");
if (_form) {
const set = (key, value) => {
_form.querySelector(`input[name=${key}]`).value = value;
};
set("username", this.accountUsername);
set("password", this.accountPassword);
const destinationUrl = cookie("destination_url");
if (destinationUrl) {
removeCookie("destination_url");
set("redirect", destinationUrl);
} else {
set("redirect", userPath("account-created"));
}
_form.submit();
}
return new Promise(() => {}); // This will never resolve, the page will reload instead
} else {
this.set("flash", result.message || i18n("create_account.failed"));
if (result.is_developer) {
this.isDeveloper = true;
}
if (
result.errors &&
result.errors.email &&
result.errors.email.length > 0 &&
result.values
) {
this.rejectedEmails.pushObject(result.values.email);
}
if (result.errors?.["user_password.password"]?.length > 0) {
this.passwordValidationHelper.rejectedPasswords.push(
attrs.accountPassword

View file

@ -1,17 +1,15 @@
import EmberObject from "@ember/object";
import { next } from "@ember/runloop";
import { htmlSafe } from "@ember/template";
import cookie, { removeCookie } from "discourse/lib/cookie";
import DiscourseUrl from "discourse/lib/url";
import getURL from "discourse/lib/get-url";
import { i18n } from "discourse-i18n";
// This is happening outside of the app via popup
const AuthErrors = [
"requires_invite",
"awaiting_approval",
"awaiting_activation",
"admin_not_allowed_from_ip_address",
"awaiting_activation",
"awaiting_approval",
"not_allowed_from_ip_address",
"requires_invite",
];
const beforeAuthCompleteCallbacks = [];
@ -27,13 +25,8 @@ export function resetBeforeAuthCompleteCallbacks() {
export default {
after: "inject-objects",
initialize(owner) {
let lastAuthResult;
if (document.getElementById("data-authentication")) {
// Happens for full screen logins
lastAuthResult = document.getElementById("data-authentication").dataset
.authenticationData;
}
const lastAuthResult = document.getElementById("data-authentication")
?.dataset?.authenticationData;
if (lastAuthResult) {
const router = owner.lookup("service:router");
@ -52,32 +45,31 @@ export default {
} else {
const siteSettings = owner.lookup("service:site-settings");
const loginError = (errorMsg, className, properties, callback) => {
const applicationController = owner.lookup(
"controller:application"
);
const loginProps = {
canSignUp: applicationController.canSignUp,
flash: errorMsg,
flashType: className || "success",
const loginError = (flash, properties, callback) => {
const props = {
flash,
flashType: "error",
awaitingApproval: options.awaiting_approval,
...properties,
};
router.transitionTo("login").then((login) => {
Object.keys(loginProps || {}).forEach((key) => {
login.controller.set(key, loginProps[key]);
});
});
router.trigger("showLogin", props);
next(() => callback?.());
};
const error = AuthErrors.find((name) => options[name]);
if (error) {
return loginError(i18n(`login.${error}`));
}
if (options.suspended) {
return loginError(options.suspended_message);
}
if (options.omniauth_disallow_totp) {
return loginError(
i18n("login.omniauth_disallow_totp"),
"error",
{
loginName: options.email,
showLoginButtons: false,
@ -86,29 +78,14 @@ export default {
);
}
for (let i = 0; i < AuthErrors.length; i++) {
const cond = AuthErrors[i];
if (options[cond]) {
return loginError(htmlSafe(i18n(`login.${cond}`)));
}
}
if (options.suspended) {
return loginError(options.suspended_message, "error");
}
// Reload the page if we're authenticated
if (options.authenticated) {
const destinationUrl =
cookie("destination_url") || options.destination_url;
if (destinationUrl) {
// redirect client to the original URL
removeCookie("destination_url");
window.location.href = destinationUrl;
} else if (
window.location.pathname === DiscourseUrl.getURL("/login")
) {
window.location = DiscourseUrl.getURL("/");
} else if (window.location.pathname === getURL("/login")) {
window.location = getURL("/");
} else {
window.location.reload();
}
@ -116,7 +93,7 @@ export default {
}
next(() => {
const createAccountProps = {
const props = {
accountEmail: options.email,
accountUsername: options.username,
accountName: options.name,
@ -124,11 +101,10 @@ export default {
skipConfirmation: siteSettings.auth_skip_create_confirm,
};
router.transitionTo("signup").then((signup) => {
const signupController =
signup.controller || owner.lookup("controller:signup");
Object.assign(signupController, createAccountProps);
signupController.handleSkipConfirmation();
router.transitionTo("signup").then(({ controller }) => {
controller ??= owner.lookup("controller:signup");
controller.setProperties(props);
controller.handleSkipConfirmation();
});
});
}

View file

@ -8,14 +8,11 @@ export default function logout({ redirect } = {}) {
return;
}
const ctx = helperContext();
let keyValueStore = ctx.keyValueStore;
const { keyValueStore, siteSettings } = helperContext();
keyValueStore.abandonLocal();
if (!isEmpty(redirect)) {
window.location.href = redirect;
return;
}
const url = ctx.siteSettings.login_required ? "/login" : "/";
window.location.href = getURL(url);
window.location = isEmpty(redirect)
? getURL(siteSettings.login_required ? "/login" : "/")
: redirect;
}

View file

@ -784,3 +784,20 @@ export function isPrimaryTab() {
export function optionalRequire(path, name = "default") {
return require.has(path) && require(path)[name];
}
// Keep in sync with `NO_DESTINATION_COOKIE` in `app/controllers/application_controller.rb`
const NO_DESTINATION_COOKIE = [
"/login",
"/signup",
"/session/",
"/auth/",
"/uploads/",
];
export function isValidDestinationUrl(url) {
return (
url &&
url !== getURL("/") &&
!NO_DESTINATION_COOKIE.some((p) => url.startsWith(getURL(p)))
);
}

View file

@ -1,6 +1,7 @@
import EmberObject from "@ember/object";
import { Promise } from "rsvp";
import { updateCsrfToken } from "discourse/lib/ajax";
import cookie from "discourse/lib/cookie";
import discourseComputed from "discourse/lib/decorators";
import getURL from "discourse/lib/get-url";
import Session from "discourse/models/session";
@ -77,6 +78,11 @@ export default class LoginMethod extends EmberObject {
}
}
const destinationUrl = cookie("destination_url");
if (destinationUrl) {
params.origin = destinationUrl;
}
return LoginMethod.buildPostForm(getURL(`/auth/${this.name}`), params).then(
(form) => form.submit()
);

View file

@ -4,7 +4,6 @@ import KeyboardShortcutsHelp from "discourse/components/modal/keyboard-shortcuts
import NotActivatedModal from "discourse/components/modal/not-activated";
import { RouteException } from "discourse/controllers/exception";
import { setting } from "discourse/lib/computed";
import cookie from "discourse/lib/cookie";
import deprecated from "discourse/lib/deprecated";
import { getOwnerWithFallback } from "discourse/lib/get-owner";
import getURL from "discourse/lib/get-url";
@ -12,16 +11,11 @@ import logout from "discourse/lib/logout";
import mobile from "discourse/lib/mobile";
import identifySource, { consolePrefix } from "discourse/lib/source-identifier";
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 DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
function isStrictlyReadonly(site) {
return site.isReadOnly && !site.isStaffWritesOnly;
}
export default class ApplicationRoute extends DiscourseRoute {
@service capabilities;
@service clientErrorHandler;
@ -86,11 +80,15 @@ export default class ApplicationRoute extends DiscourseRoute {
@action
logout() {
if (isStrictlyReadonly(this.site)) {
const { isReadOnly, isStaffWritesOnly } = this.site;
if (isReadOnly && !isStaffWritesOnly) {
this.dialog.alert(i18n("read_only_mode.logout_disabled"));
return;
} else if (this.currentUser) {
this.currentUser
.destroySession()
.then((response) => logout({ redirect: response["redirect_url"] }));
}
this._handleLogout();
}
@action
@ -175,21 +173,17 @@ export default class ApplicationRoute extends DiscourseRoute {
}
@action
showLogin() {
if (isStrictlyReadonly(this.site)) {
this.dialog.alert(i18n("read_only_mode.login_disabled"));
return;
}
this.handleShowLogin();
showLogin(props = {}) {
const t = this.router.transitionTo("login");
t.wantsTo = true;
return t.then(({ controller }) => controller.setProperties({ ...props }));
}
@action
showCreateAccount(createAccountProps = {}) {
if (this.site.isReadOnly) {
this.dialog.alert(i18n("read_only_mode.login_disabled"));
} else {
this.handleShowCreateAccount(createAccountProps);
}
showCreateAccount(props = {}) {
const t = this.router.transitionTo("signup");
t.wantsTo = true;
return t.then(({ controller }) => controller.setProperties({ ...props }));
}
@action
@ -255,50 +249,4 @@ export default class ApplicationRoute extends DiscourseRoute {
hasGroups,
});
}
handleShowLogin() {
if (this.capabilities.isAppWebview) {
postRNWebviewMessage("showLogin", true);
}
if (this.siteSettings.enable_discourse_connect) {
const returnPath = cookie("destination_url")
? getURL("/")
: encodeURIComponent(window.location.pathname);
window.location = getURL("/session/sso?return_path=" + returnPath);
} else {
if (this.login.isOnlyOneExternalLoginMethod) {
this.login.singleExternalLogin();
} else {
this.router.transitionTo("login").then((login) => {
login.controller.set("canSignUp", this.controller.canSignUp);
});
}
}
}
handleShowCreateAccount(createAccountProps) {
if (this.siteSettings.enable_discourse_connect) {
const returnPath = encodeURIComponent(window.location.pathname);
window.location = getURL("/session/sso?return_path=" + returnPath);
} else {
if (this.login.isOnlyOneExternalLoginMethod) {
// we will automatically redirect to the external auth service
this.login.singleExternalLogin({ signup: true });
} else {
this.router.transitionTo("signup").then((signup) => {
Object.keys(createAccountProps || {}).forEach((key) => {
signup.controller.set(key, createAccountProps[key]);
});
});
}
}
}
_handleLogout() {
if (this.currentUser) {
this.currentUser
.destroySession()
.then((response) => logout({ redirect: response["redirect_url"] }));
}
}
}

View file

@ -1,43 +1,33 @@
import { action } from "@ember/object";
import { next } from "@ember/runloop";
import { service } from "@ember/service";
import AssociateAccountConfirm from "discourse/components/modal/associate-account-confirm";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import cookie from "discourse/lib/cookie";
import DiscourseRoute from "discourse/routes/discourse";
export default class AssociateAccount extends DiscourseRoute {
@service router;
export default class extends DiscourseRoute {
@service currentUser;
@service modal;
@service router;
beforeModel(transition) {
if (!this.currentUser) {
cookie("destination_url", transition.intent.url);
return this.router.replaceWith("login");
}
const params = this.paramsFor("associate-account");
this.redirectToAccount(params);
}
transition.send("showLogin");
} else {
const { token } = this.paramsFor("associate-account");
@action
async redirectToAccount(params) {
await this.router
.replaceWith(`preferences.account`, this.currentUser)
.followRedirects();
next(() => this.showAssociateAccount(params));
}
@action
async showAssociateAccount(params) {
try {
const model = await ajax(
`/associate/${encodeURIComponent(params.token)}.json`
);
this.modal.show(AssociateAccountConfirm, { model });
} catch (e) {
popupAjaxError(e);
this.router
.replaceWith("preferences.account", this.currentUser)
.followRedirects()
.then(async () => {
try {
const model = await ajax(
`/associate/${encodeURIComponent(token)}.json`
);
this.modal.show(AssociateAccountConfirm, { model });
} catch (e) {
popupAjaxError(e);
}
});
}
}
}

View file

@ -1,69 +1,90 @@
import { next } from "@ember/runloop";
import { service } from "@ember/service";
import cookie from "discourse/lib/cookie";
import getURL from "discourse/lib/get-url";
import DiscourseURL from "discourse/lib/url";
import { defaultHomepage } from "discourse/lib/utilities";
import StaticPage from "discourse/models/static-page";
import {
isValidDestinationUrl,
postRNWebviewMessage,
} from "discourse/lib/utilities";
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
export default class LoginRoute extends DiscourseRoute {
@service siteSettings;
@service router;
export default class extends DiscourseRoute {
@service capabilities;
@service dialog;
@service login;
@service router;
@service site;
@service siteSettings;
isRedirecting = false;
beforeModel(transition) {
if (transition.from) {
this.internalReferrer = this.router.urlFor(transition.from.name);
const { from, wantsTo } = transition;
const { currentUser, dialog, router } = this;
const { isReadOnly, isStaffWritesOnly } = this.site;
const { isAppWebview } = this.capabilities;
const { auth_immediately, enable_discourse_connect, login_required } =
this.siteSettings;
const { pathname: url } = window.location;
const { referrer } = document;
const { isOnlyOneExternalLoginMethod, singleExternalLogin } = this.login;
// Regular users can't log in but staff can when the site is read-only
if (isReadOnly && !isStaffWritesOnly) {
transition.abort();
dialog.alert(i18n("read_only_mode.login_disabled"));
return;
}
if (this.siteSettings.login_required) {
if (
this.login.isOnlyOneExternalLoginMethod &&
this.siteSettings.auth_immediately &&
!document.getElementById("data-authentication")?.dataset
.authenticationData
) {
this.login.singleExternalLogin();
// We're in the middle of an authentication flow
if (document.getElementById("data-authentication")) {
return;
}
// When inside a webview, it handles the login flow itself
if (isAppWebview) {
postRNWebviewMessage("showLogin", true);
return;
}
// When Discourse Connect is enabled, redirect to the SSO endpoint
if (auth_immediately && enable_discourse_connect) {
const returnPath = cookie("destination_url")
? getURL("/")
: encodeURIComponent(url);
window.location = getURL(`/session/sso?return_path=${returnPath}`);
return;
}
// Automatically store the current URL (aka. the one **before** the transition)
if (!currentUser) {
if (isValidDestinationUrl(url)) {
cookie("destination_url", url);
} else if (DiscourseURL.isInternalTopic(referrer)) {
cookie("destination_url", referrer);
}
} else if (this.login.isOnlyOneExternalLoginMethod) {
this.login.singleExternalLogin();
} else if (this.siteSettings.enable_discourse_connect) {
this.router
.replaceWith(`/${defaultHomepage()}`)
.followRedirects()
.then((e) => next(() => e.send("showLogin")));
}
}
model() {
if (this.siteSettings.login_required) {
return StaticPage.find("login");
// Automatically kick off the external login if it's the only one available
if (isOnlyOneExternalLoginMethod) {
if (auth_immediately || login_required || !from || wantsTo) {
this.isRedirecting = true;
singleExternalLogin();
} else {
router.replaceWith("discovery.login-required");
}
}
}
setupController(controller) {
super.setupController(...arguments);
const { canSignUp } = this.controllerFor("application");
controller.set("canSignUp", canSignUp);
controller.set("flashType", "");
controller.set("flash", "");
if (
this.internalReferrer ||
DiscourseURL.isInternalTopic(document.referrer)
) {
controller.set(
"referrerTopicUrl",
this.internalReferrer || document.referrer
);
// We're in the middle of an authentication flow
if (document.getElementById("data-authentication")) {
return;
}
if (this.login.isOnlyOneExternalLoginMethod) {
if (this.siteSettings.auth_immediately) {
controller.set("isRedirectingToExternalAuth", true);
} else {
controller.set("singleExternalLogin", this.login.singleExternalLogin);
}
}
controller.isRedirectingToExternalAuth = this.isRedirecting;
}
}

View file

@ -1,35 +1,34 @@
import { next } from "@ember/runloop";
import { service } from "@ember/service";
import CreateInvite from "discourse/components/modal/create-invite";
import cookie from "discourse/lib/cookie";
import { defaultHomepage } from "discourse/lib/utilities";
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
export default class extends DiscourseRoute {
@service router;
@service modal;
@service dialog;
@service currentUser;
@service dialog;
@service modal;
@service router;
async beforeModel(transition) {
if (this.currentUser) {
if (transition.from) {
// when navigating from another ember route
transition.abort();
this.#openInviteModalIfAllowed();
} else {
// when landing on this route from a full page load
this.router
.replaceWith("discovery.latest")
.followRedirects()
.then(() => {
this.#openInviteModalIfAllowed();
});
}
} else {
cookie("destination_url", window.location.href);
this.router.replaceWith("login");
if (!this.currentUser) {
transition.send("showLogin");
return;
}
// when navigating from another ember route
if (transition.from) {
transition.abort();
this.#openInviteModalIfAllowed();
return;
}
// when landing on the route from a full page load
this.router
.replaceWith(`discovery.${defaultHomepage()}`)
.followRedirects()
.then(() => this.#openInviteModalIfAllowed());
}
#openInviteModalIfAllowed() {

View file

@ -1,89 +1,65 @@
import { next } from "@ember/runloop";
import { service } from "@ember/service";
import cookie from "discourse/lib/cookie";
import { defaultHomepage } from "discourse/lib/utilities";
import Group from "discourse/models/group";
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
export default class NewMessage extends DiscourseRoute {
@service dialog;
export default class extends DiscourseRoute {
@service composer;
@service dialog;
@service router;
beforeModel(transition) {
const params = transition.to.queryParams;
const userName = params.username;
const groupName = params.groupname || params.group_name;
if (!this.currentUser) {
cookie("destination_url", window.location.href);
this.router.replaceWith("login");
transition.send("showLogin");
return;
}
const { queryParams: params } = transition.to;
// when navigating from another ember route
if (transition.from) {
transition.abort();
if (userName) {
return this.openComposer(transition, userName);
}
if (groupName) {
// send a message to a group
return Group.messageable(groupName)
.then((result) => {
if (result.messageable) {
this.openComposer(transition, groupName);
} else {
this.dialog.alert(
i18n("composer.cant_send_pm", { username: groupName })
);
}
})
.catch(() =>
this.dialog.alert(i18n("composer.create_message_error"))
);
}
return this.openComposer(transition);
} else {
this.router
.replaceWith("discovery.latest")
.followRedirects()
.then(() => {
if (userName) {
return this.openComposer(transition, userName);
}
if (groupName) {
// send a message to a group
return Group.messageable(groupName)
.then((result) => {
if (result.messageable) {
this.openComposer(transition, groupName);
} else {
this.dialog.alert(
i18n("composer.cant_send_pm", { username: groupName })
);
}
})
.catch(() =>
this.dialog.alert(i18n("composer.create_message_error"))
);
}
return this.openComposer(transition);
});
this.#openComposerWithPrefilledValues(params);
return;
}
// when landing on the route from a full page load
this.router
.replaceWith(`discovery.${defaultHomepage()}`)
.followRedirects()
.then(() => this.#openComposerWithPrefilledValues(params));
}
openComposer(transition, recipients) {
next(() => {
this.composer.openNewMessage({
recipients,
title: transition.to.queryParams.title,
body: transition.to.queryParams.body,
});
});
#openComposer({ title, body }, recipients = "") {
next(() => this.composer.openNewMessage({ recipients, title, body }));
}
#openComposerWithPrefilledValues(params) {
const username = params.username;
const groupname = params.groupname || params.group_name;
if (username) {
this.#openComposer(params, username);
return;
}
if (groupname) {
Group.messageable(groupname)
.then(({ messageable }) => {
if (messageable) {
this.#openComposer(params, groupname);
} else {
this.dialog.alert(
i18n("composer.cant_send_pm", { username: groupname })
);
}
})
.catch(() => this.dialog.alert(i18n("composer.create_message_error")));
return;
}
this.#openComposer(params);
}
}

View file

@ -1,117 +1,103 @@
import { next } from "@ember/runloop";
import { service } from "@ember/service";
import cookie from "discourse/lib/cookie";
import { defaultHomepage } from "discourse/lib/utilities";
import Category from "discourse/models/category";
import DiscourseRoute from "discourse/routes/discourse";
export default class extends DiscourseRoute {
@service composer;
@service router;
@service currentUser;
@service router;
@service site;
async beforeModel(transition) {
if (this.currentUser) {
let category;
if (this.site.lazy_load_categories) {
if (transition.to.queryParams.category_id) {
const categories = await Category.asyncFindByIds([
transition.to.queryParams.category_id,
]);
category = categories[0];
} else if (transition.to.queryParams.category) {
category = await Category.asyncFindBySlugPath(
transition.to.queryParams.category
);
}
} else {
category = this.parseCategoryFromTransition(transition);
}
if (category) {
// Using URL-based transition to avoid bug with dynamic segments and refreshModel query params
// https://github.com/emberjs/ember.js/issues/16992
this.router
.replaceWith(`/c/${category.id}`)
.followRedirects()
.then(() => {
if (this.currentUser.can_create_topic) {
this.openComposer({ transition, category });
}
});
} else if (transition.from) {
// Navigation from another ember route
transition.abort();
this.openComposer({ transition });
} else {
this.router
.replaceWith("discovery.latest")
.followRedirects()
.then(() => {
if (this.currentUser.can_create_topic) {
this.openComposer({ transition });
}
});
}
} else {
// User is not logged in
cookie("destination_url", window.location.href);
this.router.replaceWith("login");
if (!this.currentUser) {
transition.send("showLogin");
return;
}
const { queryParams: params } = transition.to;
const category = await this.#loadCategoryFromTransition(params);
if (category) {
// Using URL-based transition to avoid bug with dynamic segments and refreshModel query params
// https://github.com/emberjs/ember.js/issues/16992
this.router
.replaceWith(`/c/${category.id}`)
.followRedirects()
.then(() => {
if (this.currentUser.can_create_topic) {
this.#openComposer(params, category);
}
});
return;
}
// When navigating from another ember route
if (transition.from) {
transition.abort();
this.#openComposer(params);
return;
}
// When landing on the route from a full page load
this.router
.replaceWith(`discovery.${defaultHomepage()}`)
.followRedirects()
.then(() => {
if (this.currentUser.can_create_topic) {
this.#openComposer(params);
}
});
}
openComposer({ transition, category }) {
next(() => {
this.composer.openNewTopic({
title: transition.to.queryParams.title,
body: transition.to.queryParams.body,
category,
tags: transition.to.queryParams.tags,
});
#openComposer(params, category) {
const { title, body, tags } = params;
this.composer.set("formTemplateInitialValues", transition.to.queryParams);
next(() => {
this.composer.openNewTopic({ title, body, category, tags });
this.composer.set("formTemplateInitialValues", params);
});
}
parseCategoryFromTransition(transition) {
let category;
async #loadCategoryFromTransition(params) {
let category = null;
if (transition.to.queryParams.category_id) {
const categoryId = transition.to.queryParams.category_id;
category = Category.findById(categoryId);
} else if (transition.to.queryParams.category) {
const splitCategory = transition.to.queryParams.category.split("/");
category = this._getCategory(
splitCategory[0],
splitCategory[1],
"nameLower"
);
if (!category) {
category = this._getCategory(
splitCategory[0],
splitCategory[1],
"slug"
);
if (this.site.lazy_load_categories) {
if (params.category_id) {
category = await Category.asyncFindById(params.category_id);
} else if (params.category) {
category = await Category.asyncFindBySlugPath(params.category);
}
} else {
if (params.category_id) {
category = Category.findById(params.category_id);
} else if (params.category) {
// TODO: does this work with more than 2 levels of categories?
const [main, sub] = params.category.split("/");
category = this.#getCategory(main, sub, "nameLower");
category ||= this.#getCategory(main, sub, "slug");
}
}
return category;
}
_getCategory(mainCategory, subCategory, type) {
let category;
if (!subCategory) {
category = this.site.categories.findBy(type, mainCategory.toLowerCase());
#getCategory(main, sub, type) {
let category = null;
if (!sub) {
category = this.site.categories.findBy(type, main.toLowerCase());
} else {
const categories = this.site.categories;
const main = categories.findBy(type, mainCategory.toLowerCase());
if (main) {
const { categories } = this.site;
const parent = categories.findBy(type, main.toLowerCase());
if (parent) {
category = categories.find((item) => {
return (
item &&
item[type] === subCategory.toLowerCase() &&
item.parent_category_id === main.id
item[type] === sub.toLowerCase() &&
item.parent_category_id === parent.id
);
});
}

View file

@ -1,47 +1,103 @@
import { action } from "@ember/object";
import { next } from "@ember/runloop";
import { service } from "@ember/service";
import cookie from "discourse/lib/cookie";
import getURL from "discourse/lib/get-url";
import DiscourseURL from "discourse/lib/url";
import {
defaultHomepage,
isValidDestinationUrl,
postRNWebviewMessage,
} from "discourse/lib/utilities";
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
export default class SignupRoute extends DiscourseRoute {
@service siteSettings;
@service router;
export default class extends DiscourseRoute {
@service capabilities;
@service dialog;
@service login;
@service router;
@service site;
@service siteSettings;
authComplete = false;
isRedirecting = false;
beforeModel() {
this.authComplete = document.getElementById(
"data-authentication"
)?.dataset.authenticationData;
beforeModel(transition) {
const { from, wantsTo } = transition;
const { currentUser, dialog, router } = this;
const { isReadOnly } = this.site;
const { isAppWebview } = this.capabilities;
const {
auth_immediately,
enable_discourse_connect,
invite_only,
login_required,
} = this.siteSettings;
const { pathname: url } = window.location;
const { referrer } = document;
const { canSignUp } = this.controllerFor("application");
const { isOnlyOneExternalLoginMethod, singleExternalLogin } = this.login;
if (this.login.isOnlyOneExternalLoginMethod && !this.authComplete) {
this.login.singleExternalLogin({ signup: true });
} else {
this.showCreateAccount();
// Can't sign up when the site is read-only
if (isReadOnly) {
transition.abort();
dialog.alert(i18n("read_only_mode.login_disabled"));
return;
}
// In some cases, the user is only allowed to log in, not sign up
if (!canSignUp && (invite_only || !auth_immediately)) {
const route = `discovery.${login_required ? "login-required" : defaultHomepage()}`;
router.replaceWith(route).followRedirects();
return;
}
// We're in the middle of an authentication flow
if (document.getElementById("data-authentication")) {
return;
}
// When inside a webview, it handles the login flow itself
if (isAppWebview) {
postRNWebviewMessage("showLogin", true);
return;
}
// When Discourse Connect is enabled, redirect to the SSO endpoint
if (auth_immediately && enable_discourse_connect) {
const returnPath = cookie("destination_url")
? getURL("/")
: encodeURIComponent(url);
window.location = getURL(`/session/sso?return_path=${returnPath}`);
return;
}
// Automatically store the current URL (aka. the one **before** the transition)
if (!currentUser) {
if (isValidDestinationUrl(url)) {
cookie("destination_url", url);
} else if (DiscourseURL.isInternalTopic(referrer)) {
cookie("destination_url", referrer);
}
}
// Automatically kick off the external login if it's the only one available
if (isOnlyOneExternalLoginMethod) {
if (auth_immediately || login_required || !from || wantsTo) {
this.isRedirecting = true;
singleExternalLogin({ signup: true });
} else {
router.replaceWith("discovery.login-required");
}
}
}
setupController(controller) {
super.setupController(...arguments);
if (this.login.isOnlyOneExternalLoginMethod && !this.authComplete) {
controller.set("isRedirectingToExternalAuth", true);
// We're in the middle of an authentication flow
if (document.getElementById("data-authentication")) {
return;
}
}
@action
async showCreateAccount() {
const { canSignUp } = this.controllerFor("application");
if (!canSignUp) {
const route = await this.router
.replaceWith(
this.siteSettings.login_required ? "login" : "discovery.latest"
)
.followRedirects();
if (canSignUp) {
next(() => route.send("showCreateAccount"));
}
}
controller.isRedirectingToExternalAuth = this.isRedirecting;
}
}

View file

@ -22,12 +22,10 @@ export default RouteTemplate(
{{hideApplicationSidebar}}
{{bodyClass "login-page"}}
{{#if @controller.isRedirectingToExternalAuth}}
{{! Hide the login form if the site has only one external }}
{{! authentication method and is being automatically redirected to it }}
{{loadingSpinner}}
{{else}}
<div class="login-fullpage">
<div class="login-fullpage">
{{#if @controller.isRedirectingToExternalAuth}}
{{loadingSpinner size="large"}}
{{else}}
<FlashMessage
@flash={{@controller.flash}}
@type={{@controller.flashType}}
@ -73,7 +71,7 @@ export default RouteTemplate(
{{#if @controller.showLoginButtons}}
<LoginButtons
@externalLogin={{@controller.externalLoginAction}}
@externalLogin={{@controller.externalLogin}}
@passkeyLogin={{@controller.passkeyLogin}}
@context="login"
/>
@ -97,7 +95,7 @@ export default RouteTemplate(
<PluginOutlet
@name="login-wrapper"
@outletArgs={{lazyHash
externalLoginAction=@controller.externalLoginAction
externalLogin=@controller.externalLogin
}}
>
<LocalLoginForm
@ -118,19 +116,18 @@ export default RouteTemplate(
@otherMethodAllowed={{@controller.otherMethodAllowed}}
@showSecondFactor={{@controller.showSecondFactor}}
@handleForgotPassword={{@controller.handleForgotPassword}}
@login={{@controller.triggerLogin}}
@login={{@controller.localLogin}}
@flashChanged={{@controller.flashChanged}}
@flashTypeChanged={{@controller.flashTypeChanged}}
@securityKeyCredentialChanged={{@controller.securityKeyCredentialChanged}}
/>
</PluginOutlet>
{{#if @controller.site.desktopView}}
<LoginPageCta
@canLoginLocal={{@controller.canLoginLocal}}
@showSecurityKey={{@controller.showSecurityKey}}
@login={{@controller.triggerLogin}}
@login={{@controller.localLogin}}
@loginButtonLabel={{@controller.loginButtonLabel}}
@loginDisabled={{@controller.loginDisabled}}
@showSignupLink={{@controller.showSignupLink}}
@ -154,7 +151,7 @@ export default RouteTemplate(
{{#if @controller.hasAtLeastOneLoginButton}}
<div class="login-right-side">
<LoginButtons
@externalLogin={{@controller.externalLoginAction}}
@externalLogin={{@controller.externalLogin}}
@passkeyLogin={{@controller.passkeyLogin}}
@context="login"
/>
@ -169,7 +166,7 @@ export default RouteTemplate(
<LoginPageCta
@canLoginLocal={{@controller.canLoginLocal}}
@showSecurityKey={{@controller.showSecurityKey}}
@login={{@controller.triggerLogin}}
@login={{@controller.localLogin}}
@loginButtonLabel={{@controller.loginButtonLabel}}
@loginDisabled={{@controller.loginDisabled}}
@showSignupLink={{@controller.showSignupLink}}
@ -182,7 +179,7 @@ export default RouteTemplate(
</div>
<PluginOutlet @name="below-login-page" />
</div>
{{/if}}
{{/if}}
</div>
</template>
);

View file

@ -33,12 +33,10 @@ export default RouteTemplate(
{{hideApplicationSidebar}}
{{bodyClass "signup-page"}}
{{#if @controller.isRedirectingToExternalAuth}}
{{! Hide the signup form if the site has only one external }}
{{! authentication method and is being automatically redirected to it }}
{{loadingSpinner}}
{{else}}
<div class="signup-fullpage">
<div class="signup-fullpage">
{{#if @controller.isRedirectingToExternalAuth}}
{{loadingSpinner size="large"}}
{{else}}
<FlashMessage
@flash={{@controller.flash}}
@type={{@controller.flashType}}
@ -342,7 +340,7 @@ export default RouteTemplate(
/>
{{/if}}
</div>
</div>
{{/if}}
{{/if}}
</div>
</template>
);

View file

@ -113,7 +113,7 @@ acceptance("Create Account", function () {
sinon.stub(LoginMethod, "buildPostForm").callsFake((url, params) => {
assert.step("buildPostForm");
assert.strictEqual(url, "/auth/facebook");
assert.deepEqual(params, { signup: true });
assert.true(params.signup);
});
await visit("/signup");

View file

@ -732,8 +732,15 @@ class ApplicationController < ActionController::Base
raise Discourse::InvalidAccess.new unless SiteSetting.wizard_enabled?
end
# Keep in sync with `NO_DESTINATION_COOKIE` in `app/assets/javascripts/discourse/app/lib/utilities.js`
NO_DESTINATION_COOKIE = %w[/login /signup /session/ /auth/ /uploads/].freeze
def is_valid_destination_url?(url)
url.present? && url != path("/") && NO_DESTINATION_COOKIE.none? { url.start_with? path(_1) }
end
def destination_url
request.original_url unless request.original_url =~ /uploads/
request.original_url if is_valid_destination_url?(request.original_url)
end
def redirect_to_login
@ -744,7 +751,7 @@ class ApplicationController < ActionController::Base
session[:destination_url] = destination_url
redirect_to path("/session/sso")
elsif SiteSetting.auth_immediately && !SiteSetting.enable_local_logins &&
Discourse.enabled_authenticators.length == 1 && !cookies[:authentication_data]
Discourse.enabled_authenticators.one? && !cookies[:authentication_data]
# Only one authentication provider, direct straight to it.
# If authentication_data is present, then we are halfway though registration. Don't redirect offsite
cookies[:destination_url] = destination_url

View file

@ -219,9 +219,8 @@ class Users::OmniauthCallbacksController < ApplicationController
def persist_auth_token(auth)
secret = SecureRandom.hex
secure_session.set "#{Users::AssociateAccountsController.key(secret)}",
auth.to_json,
expires: 10.minutes
key = Users::AssociateAccountsController.key(secret)
secure_session.set key, auth.to_json, expires: 10.minutes
"#{Discourse.base_path}/associate/#{secret}"
end
end

View file

@ -21,7 +21,7 @@ OmniAuth.config.before_request_phase do |env|
request.session[:auth_reconnect] = !!request.params["reconnect"]
# If the client provided an origin, store in session to redirect back
request.session[:destination_url] = request.params["origin"]
request.session[:destination_url] = request.params["origin"] if request.params["origin"].present?
end
OmniAuth.config.on_failure do |env|

View file

@ -96,22 +96,13 @@ class Auth::ManagedAuthenticator < Auth::Authenticator
retrieve_profile(association.user, association.info)
# Build the Auth::Result object
result = Auth::Result.new
info = auth_token[:info]
result = Auth::Result.new
result.email = info[:email]
result.name =
(
if (info[:first_name] && info[:last_name])
"#{info[:first_name]} #{info[:last_name]}"
else
info[:name]
end
)
if result.name.present? && result.name == result.email
# Some IDPs send the email address in the name parameter (e.g. Auth0 with default configuration)
# We add some generic protection here, so that users don't accidently make their email addresses public
result.name = nil
end
result.name = "#{info[:first_name]} #{info[:last_name]}".presence || info[:name]
# Some IDPs send the email address in the name parameter (e.g. Auth0 with default configuration)
# We add some generic protection here, so that users don't accidently make their email addresses public
result.name = nil if result.name.present? && result.name == result.email
result.username = info[:nickname]
result.email_valid = primary_email_verified?(auth_token) if result.email.present?
result.overrides_email = always_update_user_email?

View file

@ -35,8 +35,7 @@ class Middleware::OmniauthBypassMiddleware
return @app.call(env) unless env["PATH_INFO"].start_with?("/auth")
# When only one provider is enabled, assume it can be completely trusted, and allow GET requests
only_one_provider =
!SiteSetting.enable_local_logins && Discourse.enabled_authenticators.length == 1
only_one_provider = !SiteSetting.enable_local_logins && Discourse.enabled_authenticators.one?
allow_get = only_one_provider || !SiteSetting.auth_require_interaction

View file

@ -1,4 +1,3 @@
# encoding: utf-8
# frozen_string_literal: true
module Slug

View file

@ -1,5 +1,4 @@
import { service } from "@ember/service";
import cookie from "discourse/lib/cookie";
import UserTopicListRoute from "discourse/routes/user-topic-list";
import { i18n } from "discourse-i18n";
@ -14,8 +13,7 @@ export default class UserActivityAssigned extends UserTopicListRoute {
beforeModel() {
if (!this.currentUser) {
cookie("destination_url", window.location.href);
this.router.transitionTo("login");
this.send("showLogin");
}
}

View file

@ -1,6 +1,7 @@
import { service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { defaultHomepage } from "discourse/lib/utilities";
import DiscourseRoute from "discourse/routes/discourse";
export default class Transcript extends DiscourseRoute {
@ -10,18 +11,17 @@ export default class Transcript extends DiscourseRoute {
async model(params) {
if (!this.currentUser) {
this.session.set("shouldRedirectToUrl", window.location.href);
this.router.replaceWith("login");
this.send("showLogin");
return;
}
await this.router.replaceWith("discovery.latest").followRedirects();
await this.router
.replaceWith(`discovery.${defaultHomepage()}`)
.followRedirects();
try {
const result = await ajax(`/chat-transcript/${params.secret}`);
this.composer.openNewTopic({
body: result.content,
});
const { content: body } = await ajax(`/chat-transcript/${params.secret}`);
this.composer.openNewTopic({ body });
} catch (e) {
popupAjaxError(e);
}

View file

@ -271,8 +271,7 @@ export default class DiscourseReactionsActions extends Component {
toggle(params) {
if (!this.currentUser) {
if (this.args.showLogin) {
this.args.showLogin();
return;
return this.args.showLogin();
}
}
@ -453,8 +452,7 @@ export default class DiscourseReactionsActions extends Component {
toggleFromButton(attrs) {
if (!this.currentUser) {
if (this.args.showLogin) {
this.args.showLogin();
return;
return this.args.showLogin();
}
}

View file

@ -2,7 +2,6 @@ import Component from "@glimmer/component";
import { action } from "@ember/object";
import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import cookie from "discourse/lib/cookie";
import { applyBehaviorTransformer } from "discourse/lib/transformer";
import { i18n } from "discourse-i18n";
@ -63,9 +62,7 @@ export default class VoteBox extends Component {
click() {
applyBehaviorTransformer("topic-vote-button-click", () => {
if (!this.currentUser) {
cookie("destination_url", window.location.href, { path: "/" });
this.args.showLogin();
return;
return this.args.showLogin();
}
const { topic } = this.args;

View file

@ -9,7 +9,6 @@ import AsyncContent from "discourse/components/async-content";
import SmallUserList from "discourse/components/small-user-list";
import concatClass from "discourse/helpers/concat-class";
import { ajax } from "discourse/lib/ajax";
import cookie from "discourse/lib/cookie";
import { bind } from "discourse/lib/decorators";
import getURL from "discourse/lib/get-url";
import closeOnClickOutside from "discourse/modifiers/close-on-click-outside";
@ -45,9 +44,7 @@ export default class VoteBox extends Component {
event.stopPropagation();
if (!this.currentUser) {
cookie("destination_url", window.location.href, { path: "/" });
this.args.showLogin();
return;
return this.args.showLogin();
}
if (this.showWhoVoted) {

View file

@ -1,8 +1,8 @@
# frozen_string_literal: true
shared_examples "signup scenarios" do
let(:signup_form) { PageObjects::Pages::Signup.new }
let(:login_form) { PageObjects::Pages::Login.new }
let(:signup_page) { PageObjects::Pages::Signup.new }
let(:login_page) { PageObjects::Pages::Login.new }
let(:invite_form) { PageObjects::Pages::InviteForm.new }
let(:activate_account) { PageObjects::Pages::ActivateAccount.new }
let(:invite) { Fabricate(:invite) }
@ -12,27 +12,27 @@ shared_examples "signup scenarios" do
before { Jobs.run_immediately! }
it "can signup" do
signup_form
signup_page
.open
.fill_email(invite.email)
.fill_username("john")
.fill_password("supersecurepassword")
expect(signup_form).to have_valid_fields
expect(signup_page).to have_valid_fields
signup_form.click_create_account
signup_page.click_create_account
expect(page).to have_current_path("/u/account-created")
end
it "can signup and activate account" do
signup_form
signup_page
.open
.fill_email(invite.email)
.fill_username("john")
.fill_password("supersecurepassword")
expect(signup_form).to have_valid_fields
expect(signup_page).to have_valid_fields
signup_form.click_create_account
signup_page.click_create_account
expect(page).to have_current_path("/u/account-created")
mail = ActionMailer::Base.deliveries.first
@ -49,14 +49,14 @@ shared_examples "signup scenarios" do
end
it "can access 2FA preferences screen after signing up and activating account" do
signup_form
signup_page
.open
.fill_email(invite.email)
.fill_username("john")
.fill_password("supersecurepassword")
expect(signup_form).to have_valid_fields
expect(signup_page).to have_valid_fields
signup_form.click_create_account
signup_page.click_create_account
expect(page).to have_current_path("/u/account-created")
mail = ActionMailer::Base.deliveries.first
@ -102,11 +102,11 @@ shared_examples "signup scenarios" do
end
it "cannot signup with a common password" do
signup_form.open.fill_email(invite.email).fill_username("john").fill_password("0123456789")
expect(signup_form).to have_valid_fields
signup_page.open.fill_email(invite.email).fill_username("john").fill_password("0123456789")
expect(signup_page).to have_valid_fields
signup_form.click_create_account
expect(signup_form).to have_content(
signup_page.click_create_account
expect(signup_page).to have_content(
I18n.t("activerecord.errors.models.user_password.attributes.password.common"),
)
end
@ -115,30 +115,30 @@ shared_examples "signup scenarios" do
before { SiteSetting.invite_code = "cupcake" }
it "can signup with valid code" do
signup_form
signup_page
.open
.fill_email(invite.email)
.fill_username("john")
.fill_password("supersecurepassword")
.fill_code("cupcake")
expect(signup_form).to have_valid_fields
expect(signup_page).to have_valid_fields
signup_form.click_create_account
signup_page.click_create_account
expect(page).to have_current_path("/u/account-created")
end
it "cannot signup with invalid code" do
signup_form
signup_page
.open
.fill_email(invite.email)
.fill_username("john")
.fill_password("supersecurepassword")
.fill_code("pudding")
expect(signup_form).to have_valid_fields
expect(signup_page).to have_valid_fields
signup_form.click_create_account
expect(signup_form).to have_content(I18n.t("login.wrong_invite_code"))
expect(signup_form).to have_no_css(".account-created")
signup_page.click_create_account
expect(signup_page).to have_content(I18n.t("login.wrong_invite_code"))
expect(signup_page).to have_no_css(".account-created")
end
end
@ -153,31 +153,31 @@ shared_examples "signup scenarios" do
end
it "can signup when filling the custom field" do
signup_form
signup_page
.open
.fill_email(invite.email)
.fill_username("john")
.fill_password("supersecurepassword")
.fill_custom_field("Occupation", "Jedi")
expect(signup_form).to have_valid_fields
expect(signup_page).to have_valid_fields
signup_form.click_create_account
signup_page.click_create_account
expect(page).to have_current_path("/u/account-created")
end
it "cannot signup without filling the custom field" do
signup_form
signup_page
.open
.fill_email(invite.email)
.fill_username("john")
.fill_password("supersecurepassword")
expect(signup_form).to have_content("What you do for work")
expect(signup_page).to have_content("What you do for work")
signup_form.click_create_account
expect(signup_form).to have_content(I18n.t("js.user_fields.required", name: "Occupation"))
expect(signup_form).to have_no_css(".account-created")
expect(signup_form).to have_css(".tip.bad", text: "Please enter a value for \"Occupation\"")
signup_page.click_create_account
expect(signup_page).to have_content(I18n.t("js.user_fields.required", name: "Occupation"))
expect(signup_page).to have_no_css(".account-created")
expect(signup_page).to have_css(".tip.bad", text: "Please enter a value for \"Occupation\"")
end
end
@ -188,51 +188,51 @@ shared_examples "signup scenarios" do
end
it "can signup but cannot login until approval" do
signup_form
signup_page
.open
.fill_email(invite.email)
.fill_username("john")
.fill_password("supersecurepassword")
expect(signup_form).to have_valid_fields
signup_form.click_create_account
expect(signup_page).to have_valid_fields
signup_page.click_create_account
wait_for(timeout: 5) { User.find_by(username: "john") != nil }
expect(page).to have_current_path("/u/account-created")
visit "/"
login_form.open
login_form.fill_username("john")
login_form.fill_password("supersecurepassword")
login_form.click_login
expect(login_form).to have_content(I18n.t("login.not_approved"))
login_page.open
login_page.fill_username("john")
login_page.fill_password("supersecurepassword")
login_page.click_login
expect(login_page).to have_content(I18n.t("login.not_approved"))
user = User.find_by(username: "john")
user.update!(approved: true)
EmailToken.confirm(Fabricate(:email_token, user: user).token)
login_form.click_login
login_page.click_login
expect(page).to have_current_path("/")
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
it "can login directly when using an auto approved email" do
signup_form
signup_page
.open
.fill_email("johndoe@awesomeemail.com")
.fill_username("john")
.fill_password("supersecurepassword")
expect(signup_form).to have_valid_fields
expect(signup_page).to have_valid_fields
signup_form.click_create_account
signup_page.click_create_account
expect(page).to have_current_path("/u/account-created")
user = User.find_by(username: "john")
EmailToken.confirm(Fabricate(:email_token, user:).token)
visit "/"
login_form.open.fill_username("john").fill_password("supersecurepassword").click_login
login_page.open.fill_username("john").fill_password("supersecurepassword").click_login
expect(page).to have_current_path("/")
expect(page).to have_css(".header-dropdown-toggle.current-user")
@ -244,13 +244,13 @@ shared_examples "signup scenarios" do
it "can signup and activate account" do
visit("/discuss/signup")
signup_form
signup_page
.fill_email(invite.email)
.fill_username("john")
.fill_password("supersecurepassword")
expect(signup_form).to have_valid_fields
expect(signup_page).to have_valid_fields
signup_form.click_create_account
signup_page.click_create_account
expect(page).to have_current_path("/discuss/u/account-created")
mail = ActionMailer::Base.deliveries.first
@ -275,27 +275,27 @@ shared_examples "signup scenarios" do
end
it "cannot signup" do
signup_form
signup_page
.open
.fill_email("blocked@example.com")
.fill_username("john")
.fill_password("supersecurepassword")
expect(signup_form).to have_valid_username
expect(signup_form).to have_valid_password
expect(signup_form).to have_content(I18n.t("user.email.not_allowed"))
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_password
expect(signup_page).to have_content(I18n.t("user.email.not_allowed"))
end
end
context "when site is invite only" do
before { SiteSetting.invite_only = true }
it "cannot open the signup modal" do
signup_form.open
expect(signup_form).to be_closed
it "cannot open the signup page" do
signup_page.open
expect(signup_page).to be_closed
expect(page).to have_no_css(".sign-up-button")
login_form.open_from_header
expect(login_form).to have_no_css("#new-account-link")
login_page.open_from_header
expect(login_page).to have_no_css("#new-account-link")
end
it "can signup with invite link" do
@ -333,8 +333,8 @@ shared_examples "signup scenarios" do
before { SiteSetting.login_required = true }
it "displays the name field" do
signup_form.open
expect(signup_form).to have_name_input
signup_page.open
expect(signup_page).to have_name_input
end
end
@ -342,8 +342,8 @@ shared_examples "signup scenarios" do
before { SiteSetting.enable_names = false }
it "hides the name field" do
signup_form.open
expect(signup_form).to have_no_name_input
signup_page.open
expect(signup_page).to have_no_name_input
end
end
end
@ -352,8 +352,8 @@ shared_examples "signup scenarios" do
before { SiteSetting.full_name_requirement = "hidden_at_signup" }
it "hides the name field" do
signup_form.open
expect(signup_form).to have_no_name_input
signup_page.open
expect(signup_page).to have_no_name_input
end
end
@ -361,16 +361,16 @@ shared_examples "signup scenarios" do
before { SiteSetting.full_name_requirement = "required_at_signup" }
it "displays the name field" do
signup_form.open
expect(signup_form).to have_name_input
signup_page.open
expect(signup_page).to have_name_input
end
context "when enable_names is false" do
before { SiteSetting.enable_names = false }
it "hides the name field" do
signup_form.open
expect(signup_form).to have_no_name_input
signup_page.open
expect(signup_page).to have_no_name_input
end
end
end
@ -381,13 +381,13 @@ shared_examples "signup scenarios" do
user_field_text = Fabricate(:user_field)
user_field_dropdown = Fabricate(:user_field_dropdown)
signup_form.open
signup_page.open
find(".signup-page-cta__signup").click
expect(signup_form).to have_content(
expect(signup_page).to have_content(
I18n.t("js.user_fields.required", name: user_field_text.name),
)
expect(signup_form).to have_content(
expect(signup_page).to have_content(
I18n.t("js.user_fields.required_select", name: user_field_dropdown.name),
)
end

View file

@ -3,8 +3,8 @@
shared_context "with omniauth setup" do
include OmniauthHelpers
let(:login_form) { PageObjects::Pages::Login.new }
let(:signup_form) { PageObjects::Pages::Signup.new }
let(:login_page) { PageObjects::Pages::Login.new }
let(:signup_page) { PageObjects::Pages::Signup.new }
before { OmniAuth.config.test_mode = true }
end
@ -21,14 +21,14 @@ shared_examples "social authentication scenarios" do
mock_facebook_auth
visit("/")
signup_form.open.click_social_button("facebook")
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
expect(signup_form).to have_no_right_side_column
signup_page.open.click_social_button("facebook")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
expect(signup_page).to have_no_right_side_column
signup_form.click_create_account
signup_page.click_create_account
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
@ -41,14 +41,14 @@ shared_examples "social authentication scenarios" do
mock_google_auth
visit("/")
signup_form.open.click_social_button("google_oauth2")
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
expect(signup_form).to have_no_right_side_column
signup_page.open.click_social_button("google_oauth2")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
expect(signup_page).to have_no_right_side_column
signup_form.click_create_account
signup_page.click_create_account
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
@ -57,14 +57,14 @@ shared_examples "social authentication scenarios" do
mock_google_auth(verified: false)
visit("/")
signup_form.open.click_social_button("google_oauth2")
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
expect(signup_form).to have_no_right_side_column
signup_page.open.click_social_button("google_oauth2")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
expect(signup_page).to have_no_right_side_column
signup_form.click_create_account
signup_page.click_create_account
expect(page).to have_css(".account-created")
end
end
@ -78,13 +78,13 @@ shared_examples "social authentication scenarios" do
mock_github_auth
visit("/")
signup_form.open.click_social_button("github")
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
expect(signup_form).to have_no_right_side_column
signup_form.click_create_account
signup_page.open.click_social_button("github")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
expect(signup_page).to have_no_right_side_column
signup_page.click_create_account
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
@ -93,13 +93,13 @@ shared_examples "social authentication scenarios" do
mock_github_auth(verified: false)
visit("/")
signup_form.open.click_social_button("github")
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
expect(signup_form).to have_no_right_side_column
signup_form.click_create_account
signup_page.open.click_social_button("github")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
expect(signup_page).to have_no_right_side_column
signup_page.click_create_account
expect(page).to have_css(".account-created")
end
end
@ -114,17 +114,17 @@ shared_examples "social authentication scenarios" do
mock_github_auth(name: "")
visit("/")
signup_form.open.click_social_button("github")
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
expect(signup_form).to have_editable_name_input
signup_page.open.click_social_button("github")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
expect(signup_page).to have_editable_name_input
signup_form.fill_input("#new-account-name", "Test User")
signup_page.fill_input("#new-account-name", "Test User")
expect(signup_form).to have_no_right_side_column
signup_form.click_create_account
expect(signup_page).to have_no_right_side_column
signup_page.click_create_account
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
@ -132,14 +132,14 @@ shared_examples "social authentication scenarios" do
mock_github_auth(name: "Some Name")
visit("/")
signup_form.open.click_social_button("github")
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
expect(signup_form).to have_disabled_name_input
signup_page.open.click_social_button("github")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
expect(signup_page).to have_disabled_name_input
signup_form.click_create_account
signup_page.click_create_account
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
@ -153,14 +153,14 @@ shared_examples "social authentication scenarios" do
mock_twitter_auth
visit("/")
signup_form.open.click_social_button("twitter")
expect(signup_form).to be_open
expect(signup_form).to have_no_password_input
signup_form.fill_email(OmniauthHelpers::EMAIL)
expect(signup_form).to have_valid_username
expect(signup_form).to have_valid_email
expect(signup_form).to have_no_right_side_column
signup_form.click_create_account
signup_page.open.click_social_button("twitter")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
signup_page.fill_email(OmniauthHelpers::EMAIL)
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
expect(signup_page).to have_no_right_side_column
signup_page.click_create_account
expect(page).to have_css(".account-created")
end
@ -169,13 +169,13 @@ shared_examples "social authentication scenarios" do
mock_twitter_auth(verified: false)
visit("/")
signup_form.open.click_social_button("twitter")
expect(signup_form).to be_open
expect(signup_form).to have_no_password_input
signup_form.fill_email(OmniauthHelpers::EMAIL)
expect(signup_form).to have_valid_username
expect(signup_form).to have_valid_email
signup_form.click_create_account
signup_page.open.click_social_button("twitter")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
signup_page.fill_email(OmniauthHelpers::EMAIL)
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
signup_page.click_create_account
expect(page).to have_css(".account-created")
end
end
@ -189,13 +189,13 @@ shared_examples "social authentication scenarios" do
mock_discord_auth
visit("/")
signup_form.open.click_social_button("discord")
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
expect(signup_form).to have_no_right_side_column
signup_form.click_create_account
signup_page.open.click_social_button("discord")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
expect(signup_page).to have_no_right_side_column
signup_page.click_create_account
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
@ -212,13 +212,13 @@ shared_examples "social authentication scenarios" do
mock_linkedin_auth
visit("/")
signup_form.open.click_social_button("linkedin_oidc")
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
expect(signup_form).to have_no_right_side_column
signup_form.click_create_account
signup_page.open.click_social_button("linkedin_oidc")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
expect(signup_page).to have_no_right_side_column
signup_page.click_create_account
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
@ -232,13 +232,13 @@ shared_examples "social authentication scenarios" do
mock_google_auth
visit("/")
signup_form.open.click_social_button("google_oauth2")
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
expect(signup_form).to have_no_right_side_column
signup_form.click_create_account
signup_page.open.click_social_button("google_oauth2")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
expect(signup_page).to have_no_right_side_column
signup_page.click_create_account
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
@ -255,15 +255,15 @@ shared_examples "social authentication scenarios" do
mock_google_auth
visit("/")
signup_form.open.click_social_button("google_oauth2")
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
expect(signup_form).to have_disabled_username
expect(signup_form).to have_disabled_email
expect(signup_form).to have_disabled_name
signup_form.click_create_account
signup_page.open.click_social_button("google_oauth2")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
expect(signup_page).to have_disabled_username
expect(signup_page).to have_disabled_email
expect(signup_page).to have_disabled_name
signup_page.click_create_account
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
@ -279,7 +279,7 @@ shared_examples "social authentication scenarios" do
mock_google_auth
visit("/")
signup_form.open.click_social_button("google_oauth2")
signup_page.open.click_social_button("google_oauth2")
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
@ -299,23 +299,24 @@ shared_examples "social authentication scenarios" 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
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).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
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).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(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
signup_page.click_create_account
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
@ -324,22 +325,22 @@ shared_examples "social authentication scenarios" do
mock_google_auth
visit("/login")
expect(page).to have_css(".btn-social")
expect(signup_page).to be_open
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
expect(signup_page).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(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
signup_page.click_create_account
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
@ -348,11 +349,11 @@ shared_examples "social authentication scenarios" do
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(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
signup_page.click_create_account
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
@ -366,37 +367,51 @@ shared_examples "social authentication scenarios" do
end
end
it "automatically redirects when using the login button" do
it "automatically redirects when using the login button or the routes" 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(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
visit("/login")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
visit("/signup")
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
signup_page.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
it "redirects the user back to the last page they visited" 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
category = Fabricate(:category)
visit(category.url)
find(".header-buttons .login-button").click
expect(signup_page).to be_open
expect(signup_page).to have_no_password_input
expect(signup_page).to have_valid_username
expect(signup_page).to have_valid_email
signup_page.click_create_account
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")
expect(page).to have_current_path(category.url)
end
end
end
@ -419,7 +434,7 @@ shared_examples "social authentication scenarios" do
mock_facebook_auth
visit("/")
signup_form.open.click_social_button("facebook")
signup_page.open.click_social_button("facebook")
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
@ -432,7 +447,7 @@ shared_examples "social authentication scenarios" do
mock_google_auth
visit("/")
signup_form.open.click_social_button("google_oauth2")
signup_page.open.click_social_button("google_oauth2")
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
@ -445,7 +460,7 @@ shared_examples "social authentication scenarios" do
mock_github_auth
visit("/")
signup_form.open.click_social_button("github")
signup_page.open.click_social_button("github")
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
@ -464,7 +479,7 @@ shared_examples "social authentication scenarios" do
mock_twitter_auth
visit("/")
signup_form.open.click_social_button("twitter")
signup_page.open.click_social_button("twitter")
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
@ -477,7 +492,7 @@ shared_examples "social authentication scenarios" do
mock_discord_auth
visit("/")
signup_form.open.click_social_button("discord")
signup_page.open.click_social_button("discord")
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
@ -494,7 +509,7 @@ shared_examples "social authentication scenarios" do
mock_linkedin_auth
visit("/")
signup_form.open.click_social_button("linkedin_oidc")
signup_page.open.click_social_button("linkedin_oidc")
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
end
@ -504,11 +519,11 @@ end
describe "Social authentication", type: :system do
before { SiteSetting.full_name_requirement = "optional_at_signup" }
context "when fullpage desktop" do
context "when desktop" do
include_examples "social authentication scenarios"
end
context "when fullpage mobile", mobile: true do
context "when mobile", mobile: true do
include_examples "social authentication scenarios"
end
end

View file

@ -70,8 +70,6 @@ describe "User preferences | Security", type: :system do
hasResidentKey: true,
isUserVerified: true,
) do
add_cookie(name: "destination_url", value: "/new")
user_preferences_security_page.visit(user)
find(".pref-passkeys__add .btn").click
@ -104,6 +102,12 @@ describe "User preferences | Security", type: :system do
user_menu.sign_out
# ensures /hot isn't the homepage (otherwise the test below is pointless)
expect(SiteSetting.top_menu_items.first).not_to eq("hot")
# visit /hot to ensure we have a destination_url cookie set
visit("/hot")
# login with the key we just created
# this triggers the conditional UI for passkeys
# which uses the virtual authenticator
@ -112,7 +116,7 @@ describe "User preferences | Security", type: :system do
expect(page).to have_css(".header-dropdown-toggle.current-user")
# ensures that we are redirected to the destination_url cookie
expect(page.driver.current_url).to include("/new")
expect(page.driver.current_url).to include("/hot")
end
end
end