mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
UX: Add option to use fullpages for login and signup (#29034)
This adds dedicated routes for /login and /signup, replacing the use of modals. Currently, this is behind the experimental_full_page_login feature flag. It also includes some small consistency fixes related to formatting, spacing, icons, and the loading of certain elements
This commit is contained in:
parent
ec75c442db
commit
7e1cca87a9
@ -1,6 +1,5 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import { concat } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
@ -10,6 +9,7 @@ import { modifier as modifierFn } from "ember-modifier";
|
||||
import { and, not, or } from "truth-helpers";
|
||||
import ConditionalInElement from "discourse/components/conditional-in-element";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import FlashMessage from "discourse/components/flash-message";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import element from "discourse/helpers/element";
|
||||
import {
|
||||
@ -27,8 +27,6 @@ export const CLOSE_INITIATED_BY_CLICK_OUTSIDE = "initiatedByClickOut";
|
||||
export const CLOSE_INITIATED_BY_MODAL_SHOW = "initiatedByModalShow";
|
||||
export const CLOSE_INITIATED_BY_SWIPE_DOWN = "initiatedBySwipeDown";
|
||||
|
||||
const FLASH_TYPES = ["success", "error", "warning", "info"];
|
||||
|
||||
const SWIPE_VELOCITY_THRESHOLD = 0.4;
|
||||
|
||||
export default class DModal extends Component {
|
||||
@ -210,13 +208,6 @@ export default class DModal extends Component {
|
||||
this.closeModal(CLOSE_INITIATED_BY_BUTTON);
|
||||
}
|
||||
|
||||
@action
|
||||
validateFlashType(type) {
|
||||
if (type && !FLASH_TYPES.includes(type)) {
|
||||
throw `@flashType must be one of ${FLASH_TYPES.join(", ")}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Could be optimised to remove classic component once RFC389 is implemented
|
||||
// https://rfcs.emberjs.com/id/0389-dynamic-tag-names
|
||||
@cached
|
||||
@ -361,19 +352,12 @@ export default class DModal extends Component {
|
||||
|
||||
{{yield to="belowHeader"}}
|
||||
|
||||
{{this.validateFlashType @flashType}}
|
||||
{{#if @flash}}
|
||||
<div
|
||||
id="modal-alert"
|
||||
role="alert"
|
||||
class={{concatClass
|
||||
"alert"
|
||||
(if @flashType (concat "alert-" @flashType))
|
||||
}}
|
||||
>
|
||||
{{~@flash~}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<FlashMessage
|
||||
id="modal-alert"
|
||||
role="alert"
|
||||
@flash={{@flash}}
|
||||
@type={{@flashType}}
|
||||
/>
|
||||
|
||||
<div
|
||||
class={{concatClass "d-modal__body" @bodyClass}}
|
||||
|
@ -0,0 +1,21 @@
|
||||
import Component from "@glimmer/component";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
|
||||
const FLASH_TYPES = ["success", "error", "warning", "info"];
|
||||
|
||||
export default class FlashMessage extends Component {
|
||||
get flashClass() {
|
||||
if (this.args.type && !FLASH_TYPES.includes(this.args.type)) {
|
||||
throw `@type must be one of ${FLASH_TYPES.join(", ")}`;
|
||||
}
|
||||
return this.args.type ? `alert-${this.args.type}` : null;
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if @flash}}
|
||||
<div class={{concatClass "alert" this.flashClass}} ...attributes>
|
||||
{{~@flash~}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
@ -37,6 +37,10 @@ export default class Icons extends Component {
|
||||
// NOTE: In this scenario, we are forcing the sidebar on admin users,
|
||||
// so we need to still show the hamburger menu to be able to
|
||||
// access the legacy hamburger forum menu.
|
||||
if (this.header.headerButtonsHidden.includes("menu")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.args.sidebarEnabled &&
|
||||
this.sidebarState.adminSidebarAllowedWithLegacyNavigationMenu
|
||||
|
@ -50,7 +50,7 @@
|
||||
<a
|
||||
href
|
||||
id="forgot-password-link"
|
||||
tabindex="3"
|
||||
tabindex="2"
|
||||
{{on "click" this.handleForgotPassword}}
|
||||
>
|
||||
{{i18n "forgot_password.action"}}
|
||||
@ -68,32 +68,33 @@
|
||||
{{i18n "login.caps_lock_warning"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<SecondFactorForm
|
||||
@secondFactorMethod={{@secondFactorMethod}}
|
||||
@secondFactorToken={{@secondFactorToken}}
|
||||
@backupEnabled={{@backupEnabled}}
|
||||
@totpEnabled={{@totpEnabled}}
|
||||
@isLogin={{true}}
|
||||
class={{this.secondFactorClass}}
|
||||
>
|
||||
{{#if @showSecurityKey}}
|
||||
<SecurityKeyForm
|
||||
@setShowSecurityKey={{fn (mut @showSecurityKey)}}
|
||||
@setShowSecondFactor={{fn (mut @showSecondFactor)}}
|
||||
@setSecondFactorMethod={{fn (mut @secondFactorMethod)}}
|
||||
@backupEnabled={{@backupEnabled}}
|
||||
@totpEnabled={{@totpEnabled}}
|
||||
@otherMethodAllowed={{@otherMethodAllowed}}
|
||||
@action={{this.authenticateSecurityKey}}
|
||||
/>
|
||||
{{else}}
|
||||
<SecondFactorInput
|
||||
{{on "keydown" this.loginOnEnter}}
|
||||
{{on "input" (with-event-value (fn (mut @secondFactorToken)))}}
|
||||
@secondFactorMethod={{@secondFactorMethod}}
|
||||
value={{@secondFactorToken}}
|
||||
id="login-second-factor"
|
||||
/>
|
||||
{{/if}}
|
||||
</SecondFactorForm>
|
||||
{{#if this.showSecondFactorForm}}
|
||||
<SecondFactorForm
|
||||
@secondFactorMethod={{@secondFactorMethod}}
|
||||
@secondFactorToken={{@secondFactorToken}}
|
||||
@backupEnabled={{@backupEnabled}}
|
||||
@totpEnabled={{@totpEnabled}}
|
||||
@isLogin={{true}}
|
||||
>
|
||||
{{#if @showSecurityKey}}
|
||||
<SecurityKeyForm
|
||||
@setShowSecurityKey={{fn (mut @showSecurityKey)}}
|
||||
@setShowSecondFactor={{fn (mut @showSecondFactor)}}
|
||||
@setSecondFactorMethod={{fn (mut @secondFactorMethod)}}
|
||||
@backupEnabled={{@backupEnabled}}
|
||||
@totpEnabled={{@totpEnabled}}
|
||||
@otherMethodAllowed={{@otherMethodAllowed}}
|
||||
@action={{this.authenticateSecurityKey}}
|
||||
/>
|
||||
{{else}}
|
||||
<SecondFactorInput
|
||||
{{on "keydown" this.loginOnEnter}}
|
||||
{{on "input" (with-event-value (fn (mut @secondFactorToken)))}}
|
||||
@secondFactorMethod={{@secondFactorMethod}}
|
||||
value={{@secondFactorToken}}
|
||||
id="login-second-factor"
|
||||
/>
|
||||
{{/if}}
|
||||
</SecondFactorForm>
|
||||
{{/if}}
|
||||
</form>
|
@ -11,7 +11,7 @@ import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { getWebauthnCredential } from "discourse/lib/webauthn";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class LocalLoginBody extends Component {
|
||||
export default class LocalLoginForm extends Component {
|
||||
@service modal;
|
||||
|
||||
@tracked maskPassword = true;
|
||||
@ -24,10 +24,8 @@ export default class LocalLoginBody extends Component {
|
||||
: "";
|
||||
}
|
||||
|
||||
get secondFactorClass() {
|
||||
return this.args.showSecondFactor || this.args.showSecurityKey
|
||||
? ""
|
||||
: "hidden";
|
||||
get showSecondFactorForm() {
|
||||
return this.args.showSecondFactor || this.args.showSecurityKey;
|
||||
}
|
||||
|
||||
get disableLoginFields() {
|
@ -0,0 +1,41 @@
|
||||
import DButton from "discourse/components/d-button";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
||||
const LoginPageCta = <template>
|
||||
<div class="login-page-cta">
|
||||
<div class="login-page-cta__buttons">
|
||||
{{#if @canLoginLocal}}
|
||||
{{#unless @showSecurityKey}}
|
||||
<DButton
|
||||
@action={{@login}}
|
||||
@disabled={{@loginDisabled}}
|
||||
@isLoading={{@loggingIn}}
|
||||
@label={{@loginButtonLabel}}
|
||||
id="login-button"
|
||||
form="login-form"
|
||||
class="btn-large btn-primary login-page-cta__login"
|
||||
tabindex={{unless @showSecondFactor "2"}}
|
||||
/>
|
||||
{{/unless}}
|
||||
|
||||
{{#if @showSignupLink}}
|
||||
<span class="signup-page-cta__no-account-yet">
|
||||
{{i18n "create_account.no_account_yet"}}
|
||||
</span>
|
||||
<DButton
|
||||
@action={{@createAccount}}
|
||||
@disabled={{@loggingIn}}
|
||||
@label="create_account.title"
|
||||
class="btn-large btn-flat login-page-cta__signup"
|
||||
id="new-account-link"
|
||||
tabindex="3"
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<PluginOutlet @name="login-after-modal-footer" @connectorTagName="div" />
|
||||
</div>
|
||||
</template>;
|
||||
|
||||
export default LoginPageCta;
|
@ -58,7 +58,7 @@
|
||||
/>
|
||||
</WelcomeHeader>
|
||||
{{/if}}
|
||||
<Modal::Login::LocalLoginForm
|
||||
<LocalLoginForm
|
||||
@loginName={{this.loginName}}
|
||||
@loginNameChanged={{this.loginNameChanged}}
|
||||
@canLoginLocalWithEmail={{this.canLoginLocalWithEmail}}
|
||||
|
@ -333,7 +333,7 @@ export default class Login extends Component {
|
||||
}
|
||||
|
||||
@action
|
||||
async externalLoginAction(loginMethod) {
|
||||
externalLoginAction(loginMethod) {
|
||||
if (this.loginDisabled) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,7 +1,12 @@
|
||||
import Component from "@glimmer/component";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
|
||||
|
||||
export default class SecondFactorInput extends Component {
|
||||
_focusInput(input) {
|
||||
input.focus();
|
||||
}
|
||||
|
||||
get isTotp() {
|
||||
return this.args.secondFactorMethod === SECOND_FACTOR_METHODS.TOTP;
|
||||
}
|
||||
@ -45,6 +50,7 @@ export default class SecondFactorInput extends Component {
|
||||
autofocus="autofocus"
|
||||
class="second-factor-token-input"
|
||||
...attributes
|
||||
{{didInsert this._focusInput}}
|
||||
/>
|
||||
</template>
|
||||
}
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import routeAction from "discourse/helpers/route-action";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
||||
const SignupPageCta = <template>
|
||||
<div class="signup-page-cta">
|
||||
{{#if @disclaimerHtml}}
|
||||
<div class="signup-page-cta__disclaimer">
|
||||
{{htmlSafe @disclaimerHtml}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="signup-page-cta__buttons">
|
||||
<DButton
|
||||
@action={{@createAccount}}
|
||||
@disabled={{@submitDisabled}}
|
||||
@isLoading={{@formSubmitted}}
|
||||
@label="create_account.title"
|
||||
class="btn-large btn-primary signup-page-cta__signup"
|
||||
/>
|
||||
{{#unless @hasAuthOptions}}
|
||||
<span class="signup-page-cta__existing-account">
|
||||
{{i18n "create_account.already_have_account"}}
|
||||
</span>
|
||||
<DButton
|
||||
@action={{routeAction "showLogin"}}
|
||||
@disabled={{@formSubmitted}}
|
||||
@label="log_in"
|
||||
class="btn-large btn-flat signup-page-cta__login"
|
||||
/>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
<PluginOutlet
|
||||
@name="create-account-after-modal-footer"
|
||||
@connectorTagName="div"
|
||||
/>
|
||||
</template>;
|
||||
|
||||
export default SignupPageCta;
|
@ -1,5 +0,0 @@
|
||||
import Controller, { inject as controller } from "@ember/controller";
|
||||
|
||||
export default class LoginPageController extends Controller {
|
||||
@controller application;
|
||||
}
|
405
app/assets/javascripts/discourse/app/controllers/login.js
Normal file
405
app/assets/javascripts/discourse/app/controllers/login.js
Normal file
@ -0,0 +1,405 @@
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import Controller, { inject as controller } from "@ember/controller";
|
||||
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 cookie, { removeCookie } from "discourse/lib/cookie";
|
||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||
import { areCookiesEnabled } from "discourse/lib/utilities";
|
||||
import {
|
||||
getPasskeyCredential,
|
||||
isWebauthnSupported,
|
||||
} from "discourse/lib/webauthn";
|
||||
import { findAll } from "discourse/models/login-method";
|
||||
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
|
||||
import escape from "discourse-common/lib/escape";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class LoginPageController extends Controller {
|
||||
@service siteSettings;
|
||||
@service router;
|
||||
@service capabilities;
|
||||
@service dialog;
|
||||
@service site;
|
||||
@service login;
|
||||
@service modal;
|
||||
|
||||
@controller application;
|
||||
|
||||
@tracked loggingIn = false;
|
||||
@tracked loggedIn = false;
|
||||
@tracked showLoginButtons = true;
|
||||
@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;
|
||||
@tracked secondFactorRequired;
|
||||
@tracked backupEnabled;
|
||||
@tracked totpEnabled;
|
||||
@tracked showSecurityKey;
|
||||
@tracked securityKeyChallenge;
|
||||
@tracked securityKeyAllowedCredentialIds;
|
||||
@tracked secondFactorToken;
|
||||
@tracked flash;
|
||||
@tracked flashType;
|
||||
|
||||
get isAwaitingApproval() {
|
||||
return (
|
||||
this.awaitingApproval &&
|
||||
!this.canLoginLocal &&
|
||||
!this.canLoginLocalWithEmail
|
||||
);
|
||||
}
|
||||
|
||||
get loginDisabled() {
|
||||
return this.loggingIn || this.loggedIn;
|
||||
}
|
||||
|
||||
get bodyClasses() {
|
||||
const classes = ["login-body"];
|
||||
if (this.isAwaitingApproval) {
|
||||
classes.push("awaiting-approval");
|
||||
}
|
||||
if (
|
||||
this.hasAtLeastOneLoginButton &&
|
||||
!this.showSecondFactor &&
|
||||
!this.showSecurityKey
|
||||
) {
|
||||
classes.push("has-alt-auth");
|
||||
}
|
||||
if (!this.canLoginLocal) {
|
||||
classes.push("no-local-login");
|
||||
}
|
||||
if (this.showSecondFactor || this.showSecurityKey) {
|
||||
classes.push("second-factor");
|
||||
}
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
get canUsePasskeys() {
|
||||
return (
|
||||
this.siteSettings.enable_local_logins &&
|
||||
this.siteSettings.enable_passkeys &&
|
||||
isWebauthnSupported()
|
||||
);
|
||||
}
|
||||
|
||||
get hasAtLeastOneLoginButton() {
|
||||
return findAll().length > 0 || this.canUsePasskeys;
|
||||
}
|
||||
|
||||
get hasNoLoginOptions() {
|
||||
return !this.hasAtLeastOneLoginButton && !this.canLoginLocal;
|
||||
}
|
||||
|
||||
get loginButtonLabel() {
|
||||
return this.loggingIn ? "login.logging_in" : "login.title";
|
||||
}
|
||||
|
||||
get showSignupLink() {
|
||||
return this.canSignUp && !this.showSecondFactor;
|
||||
}
|
||||
|
||||
get adminLoginPath() {
|
||||
return getURL("/u/admin-login");
|
||||
}
|
||||
|
||||
@action
|
||||
async passkeyLogin(mediation = "optional") {
|
||||
try {
|
||||
const publicKeyCredential = await getPasskeyCredential(
|
||||
(e) => this.dialog.alert(e),
|
||||
mediation,
|
||||
this.capabilities.isFirefox
|
||||
);
|
||||
|
||||
if (publicKeyCredential) {
|
||||
let authResult;
|
||||
try {
|
||||
authResult = await ajax("/session/passkey/auth.json", {
|
||||
type: "POST",
|
||||
data: { publicKeyCredential },
|
||||
});
|
||||
} catch (e) {
|
||||
popupAjaxError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (authResult && !authResult.error) {
|
||||
const destinationUrl = cookie("destination_url");
|
||||
const ssoDestinationUrl = cookie("sso_destination_url");
|
||||
|
||||
if (ssoDestinationUrl) {
|
||||
removeCookie("sso_destination_url");
|
||||
window.location.assign(ssoDestinationUrl);
|
||||
} else if (destinationUrl) {
|
||||
removeCookie("destination_url");
|
||||
window.location.assign(destinationUrl);
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
this.dialog.alert(authResult.error);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
popupAjaxError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@action
|
||||
flashChanged(value) {
|
||||
this.flash = value;
|
||||
}
|
||||
|
||||
@action
|
||||
flashTypeChanged(value) {
|
||||
this.flashType = value;
|
||||
}
|
||||
|
||||
@action
|
||||
loginNameChanged(event) {
|
||||
this.loginName = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
showCreateAccount(createAccountProps = {}) {
|
||||
if (this.site.isReadOnly) {
|
||||
this.dialog.alert(I18n.t("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) {
|
||||
// we will automatically redirect to the external auth service
|
||||
this.login.externalLogin(this.externalLoginMethods[0], {
|
||||
signup: true,
|
||||
});
|
||||
} else {
|
||||
this.router.transitionTo("signup").then((login) => {
|
||||
Object.keys(createAccountProps || {}).forEach((key) => {
|
||||
login.controller.set(key, createAccountProps[key]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
showNotActivated(props) {
|
||||
this.modal.show(NotActivatedModal, { model: props });
|
||||
}
|
||||
|
||||
@action
|
||||
async triggerLogin() {
|
||||
if (this.loginDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEmpty(this.loginName) || isEmpty(this.loginPassword)) {
|
||||
this.flash = I18n.t("login.blank_username_or_password");
|
||||
this.flashType = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loggingIn = true;
|
||||
const result = await ajax("/session", {
|
||||
type: "POST",
|
||||
data: {
|
||||
login: this.loginName,
|
||||
password: this.loginPassword,
|
||||
second_factor_token:
|
||||
this.securityKeyCredential || this.secondFactorToken,
|
||||
second_factor_method: this.secondFactorMethod,
|
||||
timezone: moment.tz.guess(),
|
||||
},
|
||||
});
|
||||
if (result && result.error) {
|
||||
this.loggingIn = false;
|
||||
this.flash = null;
|
||||
|
||||
if (
|
||||
(result.security_key_enabled || result.totp_enabled) &&
|
||||
!this.secondFactorRequired
|
||||
) {
|
||||
this.otherMethodAllowed = result.multiple_second_factor_methods;
|
||||
this.secondFactorRequired = true;
|
||||
this.showLoginButtons = false;
|
||||
this.backupEnabled = result.backup_enabled;
|
||||
this.totpEnabled = result.totp_enabled;
|
||||
this.showSecondFactor = result.totp_enabled;
|
||||
this.showSecurityKey = result.security_key_enabled;
|
||||
this.secondFactorMethod = result.security_key_enabled
|
||||
? SECOND_FACTOR_METHODS.SECURITY_KEY
|
||||
: SECOND_FACTOR_METHODS.TOTP;
|
||||
this.securityKeyChallenge = result.challenge;
|
||||
this.securityKeyAllowedCredentialIds = result.allowed_credential_ids;
|
||||
|
||||
return;
|
||||
} else if (result.reason === "not_activated") {
|
||||
this.showNotActivated({
|
||||
username: this.loginName,
|
||||
sentTo: escape(result.sent_to_email),
|
||||
currentEmail: escape(result.current_email),
|
||||
});
|
||||
} else if (result.reason === "suspended") {
|
||||
this.dialog.alert(result.error);
|
||||
} else if (result.reason === "expired") {
|
||||
this.flash = htmlSafe(
|
||||
I18n.t("login.password_expired", {
|
||||
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;
|
||||
}
|
||||
|
||||
hiddenLoginForm.querySelector(`input[name=${key}]`).value = value;
|
||||
};
|
||||
|
||||
const destinationUrl = cookie("destination_url");
|
||||
const ssoDestinationUrl = cookie("sso_destination_url");
|
||||
|
||||
applyHiddenFormInputValue(this.loginName, "username");
|
||||
applyHiddenFormInputValue(this.loginPassword, "password");
|
||||
|
||||
if (ssoDestinationUrl) {
|
||||
removeCookie("sso_destination_url");
|
||||
window.location.assign(ssoDestinationUrl);
|
||||
return;
|
||||
} else if (destinationUrl) {
|
||||
// redirect client to the original URL
|
||||
removeCookie("destination_url");
|
||||
|
||||
applyHiddenFormInputValue(destinationUrl, "redirect");
|
||||
} else {
|
||||
applyHiddenFormInputValue(window.location.href, "redirect");
|
||||
}
|
||||
|
||||
if (hiddenLoginForm) {
|
||||
if (
|
||||
navigator.userAgent.match(/(iPad|iPhone|iPod)/g) &&
|
||||
navigator.userAgent.match(/Safari/g)
|
||||
) {
|
||||
// In case of Safari on iOS do not submit hidden login form
|
||||
window.location.href = hiddenLoginForm.querySelector(
|
||||
"input[name=redirect]"
|
||||
).value;
|
||||
} else {
|
||||
hiddenLoginForm.submit();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Failed to login
|
||||
if (e.jqXHR && e.jqXHR.status === 429) {
|
||||
this.flash = I18n.t("login.rate_limit");
|
||||
this.flashType = "error";
|
||||
} else if (
|
||||
e.jqXHR &&
|
||||
e.jqXHR.status === 503 &&
|
||||
e.jqXHR.responseJSON.error_type === "read_only"
|
||||
) {
|
||||
this.flash = I18n.t("read_only_mode.login_disabled");
|
||||
this.flashType = "error";
|
||||
} else if (!areCookiesEnabled()) {
|
||||
this.flash = I18n.t("login.cookies_error");
|
||||
this.flashType = "error";
|
||||
} else {
|
||||
this.flash = I18n.t("login.error");
|
||||
this.flashType = "error";
|
||||
}
|
||||
this.loggingIn = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
externalLoginAction(loginMethod) {
|
||||
if (this.loginDisabled) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
} 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
469
app/assets/javascripts/discourse/app/controllers/signup.js
Normal file
469
app/assets/javascripts/discourse/app/controllers/signup.js
Normal file
@ -0,0 +1,469 @@
|
||||
import { A } from "@ember/array";
|
||||
import Controller from "@ember/controller";
|
||||
import EmberObject, { action } from "@ember/object";
|
||||
import { notEmpty } from "@ember/object/computed";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { observes } from "@ember-decorators/object";
|
||||
import { Promise } from "rsvp";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { setting } from "discourse/lib/computed";
|
||||
import cookie, { removeCookie } from "discourse/lib/cookie";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import { emailValid } from "discourse/lib/utilities";
|
||||
import NameValidation from "discourse/mixins/name-validation";
|
||||
import PasswordValidation from "discourse/mixins/password-validation";
|
||||
import UserFieldsValidation from "discourse/mixins/user-fields-validation";
|
||||
import UsernameValidation from "discourse/mixins/username-validation";
|
||||
import { findAll } from "discourse/models/login-method";
|
||||
import User from "discourse/models/user";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class SignupPageController extends Controller.extend(
|
||||
PasswordValidation,
|
||||
UsernameValidation,
|
||||
NameValidation,
|
||||
UserFieldsValidation
|
||||
) {
|
||||
@service site;
|
||||
@service siteSettings;
|
||||
@service login;
|
||||
|
||||
accountChallenge = 0;
|
||||
accountHoneypot = 0;
|
||||
formSubmitted = false;
|
||||
rejectedEmails = A();
|
||||
prefilledUsername = null;
|
||||
userFields = null;
|
||||
isDeveloper = false;
|
||||
maskPassword = true;
|
||||
|
||||
@notEmpty("authOptions") hasAuthOptions;
|
||||
@setting("enable_local_logins") canCreateLocal;
|
||||
@setting("require_invite_code") requireInviteCode;
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
|
||||
if (cookie("email")) {
|
||||
this.set("accountEmail", cookie("email"));
|
||||
}
|
||||
|
||||
this.fetchConfirmationValue();
|
||||
|
||||
if (this.skipConfirmation) {
|
||||
this.performAccountCreation().finally(() =>
|
||||
this.set("skipConfirmation", false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
actionOnEnter(event) {
|
||||
if (!this.submitDisabled && event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.createAccount();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
selectKitFocus(event) {
|
||||
const target = document.getElementById(event.target.getAttribute("for"));
|
||||
if (target?.classList.contains("select-kit")) {
|
||||
event.preventDefault();
|
||||
target.querySelector(".select-kit-header").click();
|
||||
}
|
||||
}
|
||||
|
||||
@discourseComputed("hasAuthOptions", "canCreateLocal", "skipConfirmation")
|
||||
showCreateForm(hasAuthOptions, canCreateLocal, skipConfirmation) {
|
||||
return (hasAuthOptions || canCreateLocal) && !skipConfirmation;
|
||||
}
|
||||
|
||||
@discourseComputed("site.desktopView", "hasAuthOptions")
|
||||
showExternalLoginButtons(desktopView, hasAuthOptions) {
|
||||
return desktopView && !hasAuthOptions;
|
||||
}
|
||||
|
||||
@discourseComputed("formSubmitted")
|
||||
submitDisabled() {
|
||||
return this.formSubmitted;
|
||||
}
|
||||
|
||||
@discourseComputed("userFields", "hasAtLeastOneLoginButton", "hasAuthOptions")
|
||||
bodyClasses(userFields, hasAtLeastOneLoginButton, hasAuthOptions) {
|
||||
const classes = [];
|
||||
if (userFields) {
|
||||
classes.push("has-user-fields");
|
||||
}
|
||||
if (hasAtLeastOneLoginButton && !hasAuthOptions) {
|
||||
classes.push("has-alt-auth");
|
||||
}
|
||||
if (!this.canCreateLocal) {
|
||||
classes.push("no-local-logins");
|
||||
}
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
@discourseComputed("authOptions", "authOptions.can_edit_username")
|
||||
usernameDisabled(authOptions, canEditUsername) {
|
||||
return authOptions && !canEditUsername;
|
||||
}
|
||||
|
||||
@discourseComputed("authOptions", "authOptions.can_edit_name")
|
||||
nameDisabled(authOptions, canEditName) {
|
||||
return authOptions && !canEditName;
|
||||
}
|
||||
|
||||
@discourseComputed
|
||||
fullnameRequired() {
|
||||
return (
|
||||
this.siteSettings.full_name_required || this.siteSettings.enable_names
|
||||
);
|
||||
}
|
||||
|
||||
@discourseComputed("authOptions.auth_provider")
|
||||
passwordRequired(authProvider) {
|
||||
return isEmpty(authProvider);
|
||||
}
|
||||
|
||||
@discourseComputed
|
||||
disclaimerHtml() {
|
||||
if (this.site.tos_url && this.site.privacy_policy_url) {
|
||||
return I18n.t("create_account.disclaimer", {
|
||||
tos_link: this.site.tos_url,
|
||||
privacy_link: this.site.privacy_policy_url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check the email address
|
||||
@discourseComputed(
|
||||
"serverAccountEmail",
|
||||
"serverEmailValidation",
|
||||
"accountEmail",
|
||||
"rejectedEmails.[]",
|
||||
"forceValidationReason"
|
||||
)
|
||||
emailValidation(
|
||||
serverAccountEmail,
|
||||
serverEmailValidation,
|
||||
email,
|
||||
rejectedEmails,
|
||||
forceValidationReason
|
||||
) {
|
||||
const failedAttrs = {
|
||||
failed: true,
|
||||
ok: false,
|
||||
element: document.querySelector("#new-account-email"),
|
||||
};
|
||||
|
||||
if (serverAccountEmail === email && serverEmailValidation) {
|
||||
return serverEmailValidation;
|
||||
}
|
||||
|
||||
// If blank, fail without a reason
|
||||
if (isEmpty(email)) {
|
||||
return EmberObject.create(
|
||||
Object.assign(failedAttrs, {
|
||||
message: I18n.t("user.email.required"),
|
||||
reason: forceValidationReason ? I18n.t("user.email.required") : null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (rejectedEmails.includes(email) || !emailValid(email)) {
|
||||
return EmberObject.create(
|
||||
Object.assign(failedAttrs, {
|
||||
reason: I18n.t("user.email.invalid"),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
this.get("authOptions.email") === email &&
|
||||
this.get("authOptions.email_valid")
|
||||
) {
|
||||
return EmberObject.create({
|
||||
ok: true,
|
||||
reason: I18n.t("user.email.authenticated", {
|
||||
provider: this.authProviderDisplayName(
|
||||
this.get("authOptions.auth_provider")
|
||||
),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return EmberObject.create({
|
||||
ok: true,
|
||||
reason: I18n.t("user.email.ok"),
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
checkEmailAvailability() {
|
||||
if (
|
||||
!this.emailValidation.ok ||
|
||||
this.serverAccountEmail === this.accountEmail
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return User.checkEmail(this.accountEmail)
|
||||
.then((result) => {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.failed) {
|
||||
this.setProperties({
|
||||
serverAccountEmail: this.accountEmail,
|
||||
serverEmailValidation: EmberObject.create({
|
||||
failed: true,
|
||||
element: document.querySelector("#new-account-email"),
|
||||
reason: result.errors[0],
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
this.setProperties({
|
||||
serverAccountEmail: this.accountEmail,
|
||||
serverEmailValidation: EmberObject.create({
|
||||
ok: true,
|
||||
reason: I18n.t("user.email.ok"),
|
||||
}),
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.setProperties({
|
||||
serverAccountEmail: null,
|
||||
serverEmailValidation: null,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@discourseComputed(
|
||||
"accountEmail",
|
||||
"authOptions.email",
|
||||
"authOptions.email_valid"
|
||||
)
|
||||
emailDisabled() {
|
||||
return (
|
||||
this.get("authOptions.email") === this.accountEmail &&
|
||||
this.get("authOptions.email_valid")
|
||||
);
|
||||
}
|
||||
|
||||
authProviderDisplayName(providerName) {
|
||||
const matchingProvider = findAll().find((provider) => {
|
||||
return provider.name === providerName;
|
||||
});
|
||||
return matchingProvider ? matchingProvider.get("prettyName") : providerName;
|
||||
}
|
||||
|
||||
@observes("emailValidation", "accountEmail")
|
||||
prefillUsername() {
|
||||
if (this.prefilledUsername) {
|
||||
// If username field has been filled automatically, and email field just changed,
|
||||
// then remove the username.
|
||||
if (this.accountUsername === this.prefilledUsername) {
|
||||
this.set("accountUsername", "");
|
||||
}
|
||||
this.set("prefilledUsername", null);
|
||||
}
|
||||
if (
|
||||
this.get("emailValidation.ok") &&
|
||||
(isEmpty(this.accountUsername) || this.get("authOptions.email"))
|
||||
) {
|
||||
// If email is valid and username has not been entered yet,
|
||||
// or email and username were filled automatically by 3rd party auth,
|
||||
// then look for a registered username that matches the email.
|
||||
discourseDebounce(this, this.fetchExistingUsername, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Determines whether at least one login button is enabled
|
||||
@discourseComputed
|
||||
hasAtLeastOneLoginButton() {
|
||||
return findAll().length > 0;
|
||||
}
|
||||
|
||||
fetchConfirmationValue() {
|
||||
if (this._challengeDate === undefined && this._hpPromise) {
|
||||
// Request already in progress
|
||||
return this._hpPromise;
|
||||
}
|
||||
|
||||
this._hpPromise = ajax("/session/hp.json")
|
||||
.then((json) => {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._challengeDate = new Date();
|
||||
// remove 30 seconds for jitter, make sure this works for at least
|
||||
// 30 seconds so we don't have hard loops
|
||||
this._challengeExpiry = parseInt(json.expires_in, 10) - 30;
|
||||
if (this._challengeExpiry < 30) {
|
||||
this._challengeExpiry = 30;
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
accountHoneypot: json.value,
|
||||
accountChallenge: json.challenge.split("").reverse().join(""),
|
||||
});
|
||||
})
|
||||
.finally(() => (this._hpPromise = undefined));
|
||||
|
||||
return this._hpPromise;
|
||||
}
|
||||
|
||||
performAccountCreation() {
|
||||
if (
|
||||
!this._challengeDate ||
|
||||
new Date() - this._challengeDate > 1000 * this._challengeExpiry
|
||||
) {
|
||||
return this.fetchConfirmationValue().then(() =>
|
||||
this.performAccountCreation()
|
||||
);
|
||||
}
|
||||
|
||||
const attrs = {
|
||||
accountName: this.accountName,
|
||||
accountEmail: this.accountEmail,
|
||||
accountPassword: this.accountPassword,
|
||||
accountUsername: this.accountUsername,
|
||||
accountChallenge: this.accountChallenge,
|
||||
inviteCode: this.inviteCode,
|
||||
accountPasswordConfirm: this.accountHoneypot,
|
||||
};
|
||||
|
||||
const destinationUrl = this.get("authOptions.destination_url");
|
||||
|
||||
if (!isEmpty(destinationUrl)) {
|
||||
cookie("destination_url", destinationUrl, { path: "/" });
|
||||
}
|
||||
|
||||
// Add the userFields to the data
|
||||
if (!isEmpty(this.userFields)) {
|
||||
attrs.userFields = {};
|
||||
this.userFields.forEach(
|
||||
(f) => (attrs.userFields[f.get("field.id")] = f.get("value"))
|
||||
);
|
||||
}
|
||||
|
||||
this.set("formSubmitted", true);
|
||||
return User.createAccount(attrs).then(
|
||||
(result) => {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("isDeveloper", false);
|
||||
if (result.success) {
|
||||
// invalidate honeypot
|
||||
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();
|
||||
}
|
||||
return new Promise(() => {}); // This will never resolve, the page will reload instead
|
||||
} else {
|
||||
this.set("flash", result.message || I18n.t("create_account.failed"));
|
||||
if (result.is_developer) {
|
||||
this.set("isDeveloper", true);
|
||||
}
|
||||
if (
|
||||
result.errors &&
|
||||
result.errors.email &&
|
||||
result.errors.email.length > 0 &&
|
||||
result.values
|
||||
) {
|
||||
this.rejectedEmails.pushObject(result.values.email);
|
||||
}
|
||||
if (
|
||||
result.errors &&
|
||||
result.errors.password &&
|
||||
result.errors.password.length > 0
|
||||
) {
|
||||
this.rejectedPasswords.pushObject(attrs.accountPassword);
|
||||
}
|
||||
this.set("formSubmitted", false);
|
||||
removeCookie("destination_url");
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.set("formSubmitted", false);
|
||||
removeCookie("destination_url");
|
||||
return this.set("flash", I18n.t("create_account.failed"));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@discourseComputed("authOptions.associate_url", "authOptions.auth_provider")
|
||||
associateHtml(url, provider) {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
return I18n.t("create_account.associate", {
|
||||
associate_link: url,
|
||||
provider: I18n.t(`login.${provider}.name`),
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
togglePasswordMask() {
|
||||
this.toggleProperty("maskPassword");
|
||||
}
|
||||
|
||||
@action
|
||||
externalLogin(provider) {
|
||||
// we will automatically redirect to the external auth service
|
||||
this.login.externalLogin(provider, { signup: true });
|
||||
}
|
||||
|
||||
@action
|
||||
createAccount() {
|
||||
this.set("flash", "");
|
||||
this.set("forceValidationReason", true);
|
||||
|
||||
const validation = [
|
||||
this.emailValidation,
|
||||
this.usernameValidation,
|
||||
this.nameValidation,
|
||||
this.passwordValidation,
|
||||
this.userFieldsValidation,
|
||||
].find((v) => v.failed);
|
||||
|
||||
if (validation) {
|
||||
const element = validation.element;
|
||||
if (element) {
|
||||
if (element.tagName === "DIV") {
|
||||
if (element.scrollIntoView) {
|
||||
element.scrollIntoView();
|
||||
}
|
||||
element.click();
|
||||
} else {
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("forceValidationReason", false);
|
||||
this.performAccountCreation();
|
||||
}
|
||||
}
|
@ -60,19 +60,32 @@ export default {
|
||||
const applicationController = owner.lookup(
|
||||
"controller:application"
|
||||
);
|
||||
modal.show(LoginModal, {
|
||||
model: {
|
||||
showNotActivated: (props) =>
|
||||
applicationRoute.send("showNotActivated", props),
|
||||
showCreateAccount: (props) =>
|
||||
applicationRoute.send("showCreateAccount", props),
|
||||
canSignUp: applicationController.canSignUp,
|
||||
flash: errorMsg,
|
||||
flashType: className || "success",
|
||||
awaitingApproval: options.awaiting_approval,
|
||||
...properties,
|
||||
},
|
||||
});
|
||||
|
||||
const loginProps = {
|
||||
canSignUp: applicationController.canSignUp,
|
||||
flash: errorMsg,
|
||||
flashType: className || "success",
|
||||
awaitingApproval: options.awaiting_approval,
|
||||
...properties,
|
||||
};
|
||||
|
||||
if (siteSettings.experimental_full_page_login) {
|
||||
router.transitionTo("login").then((login) => {
|
||||
Object.keys(loginProps || {}).forEach((key) => {
|
||||
login.controller.set(key, loginProps[key]);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
modal.show(LoginModal, {
|
||||
model: {
|
||||
showNotActivated: (props) =>
|
||||
applicationRoute.send("showNotActivated", props),
|
||||
showCreateAccount: (props) =>
|
||||
applicationRoute.send("showCreateAccount", props),
|
||||
...loginProps,
|
||||
},
|
||||
});
|
||||
}
|
||||
next(() => callback?.());
|
||||
};
|
||||
|
||||
@ -117,17 +130,25 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
next(() =>
|
||||
modal.show(CreateAccount, {
|
||||
model: {
|
||||
accountEmail: options.email,
|
||||
accountUsername: options.username,
|
||||
accountName: options.name,
|
||||
authOptions: EmberObject.create(options),
|
||||
skipConfirmation: siteSettings.auth_skip_create_confirm,
|
||||
},
|
||||
})
|
||||
);
|
||||
next(() => {
|
||||
const createAccountProps = {
|
||||
accountEmail: options.email,
|
||||
accountUsername: options.username,
|
||||
accountName: options.name,
|
||||
authOptions: EmberObject.create(options),
|
||||
skipConfirmation: siteSettings.auth_skip_create_confirm,
|
||||
};
|
||||
|
||||
if (siteSettings.experimental_full_page_login) {
|
||||
router.transitionTo("signup").then((login) => {
|
||||
Object.keys(createAccountProps || {}).forEach((key) => {
|
||||
login.controller.set(key, createAccountProps[key]);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
modal.show(CreateAccount, { model: createAccountProps });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import CreateAccount from "discourse/components/modal/create-account";
|
||||
import ForgotPassword from "discourse/components/modal/forgot-password";
|
||||
import KeyboardShortcutsHelp from "discourse/components/modal/keyboard-shortcuts-help";
|
||||
import LoginModal from "discourse/components/modal/login";
|
||||
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";
|
||||
@ -20,7 +20,6 @@ import deprecated from "discourse-common/lib/deprecated";
|
||||
import { getOwnerWithFallback } from "discourse-common/lib/get-owner";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import I18n from "discourse-i18n";
|
||||
import NotActivatedModal from "../components/modal/not-activated";
|
||||
|
||||
function isStrictlyReadonly(site) {
|
||||
return site.isReadOnly && !site.isStaffWritesOnly;
|
||||
@ -207,11 +206,6 @@ export default class ApplicationRoute extends DiscourseRoute {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
showForgotPassword() {
|
||||
this.modal.show(ForgotPassword);
|
||||
}
|
||||
|
||||
@action
|
||||
showNotActivated(props) {
|
||||
this.modal.show(NotActivatedModal, { model: props });
|
||||
@ -303,6 +297,10 @@ export default class ApplicationRoute extends DiscourseRoute {
|
||||
} else {
|
||||
if (this.isOnlyOneExternalLoginMethod) {
|
||||
this.login.externalLogin(this.externalLoginMethods[0]);
|
||||
} else if (this.siteSettings.experimental_full_page_login) {
|
||||
this.router.transitionTo("login").then((login) => {
|
||||
login.controller.set("canSignUp", this.controller.canSignUp);
|
||||
});
|
||||
} else {
|
||||
this.modal.show(LoginModal, {
|
||||
model: {
|
||||
@ -325,6 +323,12 @@ export default class ApplicationRoute extends DiscourseRoute {
|
||||
this.login.externalLogin(this.externalLoginMethods[0], {
|
||||
signup: true,
|
||||
});
|
||||
} else if (this.siteSettings.experimental_full_page_login) {
|
||||
this.router.transitionTo("signup").then((signup) => {
|
||||
Object.keys(createAccountProps || {}).forEach((key) => {
|
||||
signup.controller.set(key, createAccountProps[key]);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.modal.show(CreateAccount, { model: createAccountProps });
|
||||
}
|
||||
|
@ -8,12 +8,11 @@ export default class LoginRoute extends DiscourseRoute {
|
||||
@service siteSettings;
|
||||
@service router;
|
||||
|
||||
// `login-page` because `login` controller is the one for
|
||||
// the login modal
|
||||
controllerName = "login-page";
|
||||
|
||||
beforeModel() {
|
||||
if (!this.siteSettings.login_required) {
|
||||
if (
|
||||
!this.siteSettings.login_required &&
|
||||
!this.siteSettings.experimental_full_page_login
|
||||
) {
|
||||
this.router
|
||||
.replaceWith(`/${defaultHomepage()}`)
|
||||
.followRedirects()
|
||||
@ -22,6 +21,17 @@ export default class LoginRoute extends DiscourseRoute {
|
||||
}
|
||||
|
||||
model() {
|
||||
return StaticPage.find("login");
|
||||
if (!this.siteSettings.experimental_full_page_login) {
|
||||
return StaticPage.find("login");
|
||||
}
|
||||
}
|
||||
|
||||
setupController(controller) {
|
||||
super.setupController(...arguments);
|
||||
|
||||
const { canSignUp } = this.controllerFor("application");
|
||||
controller.set("canSignUp", canSignUp);
|
||||
controller.set("flashType", "");
|
||||
controller.set("flash", "");
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,9 @@ export default class SignupRoute extends DiscourseRoute {
|
||||
@action
|
||||
async showCreateAccount() {
|
||||
const { canSignUp } = this.controllerFor("application");
|
||||
if (canSignUp && this.siteSettings.experimental_full_page_login) {
|
||||
return;
|
||||
}
|
||||
const route = await this.router
|
||||
.replaceWith(
|
||||
this.siteSettings.login_required ? "login" : "discovery.latest"
|
||||
|
@ -7,7 +7,7 @@ import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
import { SCROLLED_UP } from "./scroll-direction";
|
||||
|
||||
const VALID_HEADER_BUTTONS_TO_HIDE = ["search", "login", "signup"];
|
||||
const VALID_HEADER_BUTTONS_TO_HIDE = ["search", "login", "signup", "menu"];
|
||||
|
||||
@disableImplicitInjections
|
||||
export default class Header extends Service {
|
||||
|
@ -1,5 +1,5 @@
|
||||
{{body-class "account-created-page"}}
|
||||
{{hide-application-header-buttons "search" "login" "signup"}}
|
||||
{{hide-application-header-buttons "search" "login" "signup" "menu"}}
|
||||
{{hide-application-sidebar}}
|
||||
<div class="account-created">
|
||||
{{outlet}}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{{body-class "invite-page"}}
|
||||
{{hide-application-header-buttons "search" "login" "signup"}}
|
||||
{{hide-application-header-buttons "search" "login" "signup" "menu"}}
|
||||
{{hide-application-sidebar}}
|
||||
<section>
|
||||
<div class="container invites-show clearfix">
|
||||
|
@ -1,34 +1,175 @@
|
||||
{{body-class "static-login"}}
|
||||
{{#if this.siteSettings.experimental_full_page_login}}
|
||||
{{hide-application-header-buttons "search" "login" "signup" "menu"}}
|
||||
{{hide-application-sidebar}}
|
||||
|
||||
<section class="container">
|
||||
<div class="contents clearfix body-page">
|
||||
<div class="login-welcome">
|
||||
<PluginOutlet @name="above-login" @outletArgs={{hash model=this.model}} />
|
||||
<PluginOutlet @name="above-static" />
|
||||
<FlashMessage @flash={{this.flash}} @type={{this.flashType}} />
|
||||
<div class="login-fullpage">
|
||||
<div class={{concat-class "login-body" this.bodyClasses}}>
|
||||
<PluginOutlet @name="login-before-modal-body" @connectorTagName="div" />
|
||||
|
||||
<div class="login-content">
|
||||
{{html-safe this.model.html}}
|
||||
</div>
|
||||
|
||||
<PluginOutlet @name="below-static" />
|
||||
<PluginOutlet @name="below-login" @outletArgs={{hash model=this.model}} />
|
||||
|
||||
<div class="body-page-button-container">
|
||||
{{#if this.application.canSignUp}}
|
||||
<DButton
|
||||
@action={{route-action "showCreateAccount"}}
|
||||
@label="sign_up"
|
||||
class="btn-primary sign-up-button"
|
||||
/>
|
||||
{{#if this.hasNoLoginOptions}}
|
||||
<div class={{if this.site.desktopView "login-left-side"}}>
|
||||
<div class="login-welcome-header no-login-methods-configured">
|
||||
<h1 class="login-title">{{i18n "login.no_login_methods.title"}}</h1>
|
||||
<img />
|
||||
<p class="login-subheader">
|
||||
{{html-safe
|
||||
(i18n
|
||||
"login.no_login_methods.description"
|
||||
(hash adminLoginPath=this.adminLoginPath)
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if this.site.mobileView}}
|
||||
<WelcomeHeader
|
||||
@header={{i18n "login.header_title"}}
|
||||
@subheader={{i18n "login.subheader_title"}}
|
||||
>
|
||||
<PluginOutlet
|
||||
@name="login-header-bottom"
|
||||
@outletArgs={{hash createAccount=this.createAccount}}
|
||||
/>
|
||||
</WelcomeHeader>
|
||||
{{#if this.showLoginButtons}}
|
||||
<LoginButtons
|
||||
@externalLogin={{this.externalLoginAction}}
|
||||
@passkeyLogin={{this.passkeyLogin}}
|
||||
@context="login"
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<DButton
|
||||
@action={{route-action "showLogin"}}
|
||||
@icon="user"
|
||||
@label="log_in"
|
||||
class="btn-primary login-button"
|
||||
/>
|
||||
</div>
|
||||
{{#if this.canLoginLocal}}
|
||||
<div class={{if this.site.desktopView "login-left-side"}}>
|
||||
{{#if this.site.desktopView}}
|
||||
<WelcomeHeader
|
||||
@header={{i18n "login.header_title"}}
|
||||
@subheader={{i18n "login.subheader_title"}}
|
||||
>
|
||||
<PluginOutlet
|
||||
@name="login-header-bottom"
|
||||
@outletArgs={{hash createAccount=this.createAccount}}
|
||||
/>
|
||||
</WelcomeHeader>
|
||||
{{/if}}
|
||||
<LocalLoginForm
|
||||
@loginName={{this.loginName}}
|
||||
@loginNameChanged={{this.loginNameChanged}}
|
||||
@canLoginLocalWithEmail={{this.canLoginLocalWithEmail}}
|
||||
@canUsePasskeys={{this.canUsePasskeys}}
|
||||
@passkeyLogin={{this.passkeyLogin}}
|
||||
@loginPassword={{this.loginPassword}}
|
||||
@secondFactorMethod={{this.secondFactorMethod}}
|
||||
@secondFactorToken={{this.secondFactorToken}}
|
||||
@backupEnabled={{this.backupEnabled}}
|
||||
@totpEnabled={{this.totpEnabled}}
|
||||
@securityKeyAllowedCredentialIds={{this.securityKeyAllowedCredentialIds}}
|
||||
@securityKeyChallenge={{this.securityKeyChallenge}}
|
||||
@showSecurityKey={{this.showSecurityKey}}
|
||||
@otherMethodAllowed={{this.otherMethodAllowed}}
|
||||
@showSecondFactor={{this.showSecondFactor}}
|
||||
@handleForgotPassword={{this.handleForgotPassword}}
|
||||
@login={{this.triggerLogin}}
|
||||
@flashChanged={{this.flashChanged}}
|
||||
@flashTypeChanged={{this.flashTypeChanged}}
|
||||
@securityKeyCredentialChanged={{this.securityKeyCredentialChanged}}
|
||||
/>
|
||||
{{#if this.site.desktopView}}
|
||||
<LoginPageCta
|
||||
@canLoginLocal={{this.canLoginLocal}}
|
||||
@showSecurityKey={{this.showSecurityKey}}
|
||||
@login={{this.triggerLogin}}
|
||||
@loginButtonLabel={{this.loginButtonLabel}}
|
||||
@loginDisabled={{this.loginDisabled}}
|
||||
@showSignupLink={{this.showSignupLink}}
|
||||
@createAccount={{this.createAccount}}
|
||||
@loggingIn={{this.loggingIn}}
|
||||
@showSecondFactor={{this.showSecondFactor}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if (and this.showLoginButtons this.site.desktopView)}}
|
||||
{{#unless this.canLoginLocal}}
|
||||
<div class="login-left-side">
|
||||
<WelcomeHeader
|
||||
@header={{i18n "login.header_title"}}
|
||||
@subheader={{i18n "login.subheader_title"}}
|
||||
/>
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{#if this.hasAtLeastOneLoginButton}}
|
||||
<div class="login-right-side">
|
||||
<LoginButtons
|
||||
@externalLogin={{this.externalLoginAction}}
|
||||
@passkeyLogin={{this.passkeyLogin}}
|
||||
@context="login"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if this.site.mobileView}}
|
||||
{{#unless this.hasNoLoginOptions}}
|
||||
<LoginPageCta
|
||||
@canLoginLocal={{this.canLoginLocal}}
|
||||
@showSecurityKey={{this.showSecurityKey}}
|
||||
@login={{this.triggerLogin}}
|
||||
@loginButtonLabel={{this.loginButtonLabel}}
|
||||
@loginDisabled={{this.loginDisabled}}
|
||||
@showSignupLink={{this.showSignupLink}}
|
||||
@createAccount={{this.createAccount}}
|
||||
@loggingIn={{this.loggingIn}}
|
||||
@showSecondFactor={{this.showSecondFactor}}
|
||||
/>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{else}}
|
||||
{{body-class "static-login"}}
|
||||
<section class="container">
|
||||
<div class="contents clearfix body-page">
|
||||
<div class="login-welcome">
|
||||
<PluginOutlet
|
||||
@name="above-login"
|
||||
@outletArgs={{hash model=this.model}}
|
||||
/>
|
||||
<PluginOutlet @name="above-static" />
|
||||
|
||||
<div class="login-content">
|
||||
{{html-safe this.model.html}}
|
||||
</div>
|
||||
|
||||
<PluginOutlet @name="below-static" />
|
||||
<PluginOutlet
|
||||
@name="below-login"
|
||||
@outletArgs={{hash model=this.model}}
|
||||
/>
|
||||
|
||||
<div class="body-page-button-container">
|
||||
{{#if this.application.canSignUp}}
|
||||
<DButton
|
||||
@action={{route-action "showCreateAccount"}}
|
||||
@label="sign_up"
|
||||
class="btn-primary sign-up-button"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<DButton
|
||||
@action={{route-action "showLogin"}}
|
||||
@icon="user"
|
||||
@label="log_in"
|
||||
class="btn-primary login-button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/if}}
|
@ -1,6 +1,6 @@
|
||||
{{body-class "password-reset-page"}}
|
||||
{{hide-application-sidebar}}
|
||||
{{hide-application-header-buttons "search" "login" "signup"}}
|
||||
{{hide-application-header-buttons "search" "login" "signup" "menu"}}
|
||||
<div class="container password-reset clearfix">
|
||||
<div class="pull-left col-image">
|
||||
<img src={{this.lockImageUrl}} class="password-reset-img" alt="" />
|
||||
|
285
app/assets/javascripts/discourse/app/templates/signup.hbs
Normal file
285
app/assets/javascripts/discourse/app/templates/signup.hbs
Normal file
@ -0,0 +1,285 @@
|
||||
{{! template-lint-disable no-duplicate-id }}
|
||||
{{hide-application-header-buttons "search" "login" "signup" "menu"}}
|
||||
{{hide-application-sidebar}}
|
||||
|
||||
<FlashMessage @flash={{this.flash}} @type={{this.flashType}} />
|
||||
|
||||
<div class="signup-fullpage">
|
||||
<div class={{concat-class "signup-body" this.bodyClasses}}>
|
||||
<PluginOutlet
|
||||
@name="create-account-before-modal-body"
|
||||
@connectorTagName="div"
|
||||
/>
|
||||
|
||||
<div
|
||||
class={{concat-class
|
||||
(if this.site.desktopView "login-left-side")
|
||||
this.authOptions.auth_provider
|
||||
}}
|
||||
>
|
||||
<SignupProgressBar @step="signup" />
|
||||
<WelcomeHeader
|
||||
id="create-account-title"
|
||||
@header={{i18n "create_account.header_title"}}
|
||||
@subheader={{i18n "create_account.subheader_title"}}
|
||||
>
|
||||
<PluginOutlet
|
||||
@name="create-account-header-bottom"
|
||||
@outletArgs={{hash showLogin=(route-action "showLogin")}}
|
||||
/>
|
||||
</WelcomeHeader>
|
||||
{{#if this.showCreateForm}}
|
||||
<form id="login-form">
|
||||
{{#if this.associateHtml}}
|
||||
<div class="input-group create-account-associate-link">
|
||||
<span>{{html-safe this.associateHtml}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="input-group create-account-email">
|
||||
<Input
|
||||
{{on "focusout" this.checkEmailAvailability}}
|
||||
@type="email"
|
||||
@value={{this.accountEmail}}
|
||||
disabled={{this.emailDisabled}}
|
||||
autofocus="autofocus"
|
||||
aria-describedby="account-email-validation account-email-validation-more-info"
|
||||
aria-invalid={{this.emailValidation.failed}}
|
||||
name="email"
|
||||
id="new-account-email"
|
||||
class={{value-entered this.accountEmail}}
|
||||
/>
|
||||
<label class="alt-placeholder" for="new-account-email">
|
||||
{{i18n "user.email.title"}}
|
||||
</label>
|
||||
<InputTip
|
||||
@validation={{this.emailValidation}}
|
||||
id="account-email-validation"
|
||||
/>
|
||||
{{#unless this.emailValidation.reason}}
|
||||
<span class="more-info" id="account-email-validation-more-info">
|
||||
{{i18n "user.email.instructions"}}
|
||||
</span>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
<div class="input-group create-account__username">
|
||||
<Input
|
||||
@value={{this.accountUsername}}
|
||||
disabled={{this.usernameDisabled}}
|
||||
maxlength={{this.maxUsernameLength}}
|
||||
aria-describedby="username-validation username-validation-more-info"
|
||||
aria-invalid={{this.usernameValidation.failed}}
|
||||
autocomplete="off"
|
||||
name="username"
|
||||
id="new-account-username"
|
||||
class={{value-entered this.accountUsername}}
|
||||
/>
|
||||
<label class="alt-placeholder" for="new-account-username">
|
||||
{{i18n "user.username.title"}}
|
||||
</label>
|
||||
|
||||
<InputTip
|
||||
@validation={{this.usernameValidation}}
|
||||
id="username-validation"
|
||||
/>
|
||||
{{#unless this.usernameValidation.reason}}
|
||||
<span class="more-info" id="username-validation-more-info">
|
||||
{{i18n "user.username.instructions"}}
|
||||
</span>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
<PluginOutlet
|
||||
@name="create-account-before-password"
|
||||
@outletArgs={{hash
|
||||
accountName=this.accountName
|
||||
accountUsername=this.accountUsername
|
||||
accountPassword=this.accountPassword
|
||||
userFields=this.userFields
|
||||
authOptions=this.authOptions
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="input-group create-account__password">
|
||||
{{#if this.passwordRequired}}
|
||||
<PasswordField
|
||||
@value={{this.accountPassword}}
|
||||
@capsLockOn={{this.capsLockOn}}
|
||||
type={{if this.maskPassword "password" "text"}}
|
||||
autocomplete="current-password"
|
||||
aria-describedby="password-validation password-validation-more-info"
|
||||
aria-invalid={{this.passwordValidation.failed}}
|
||||
id="new-account-password"
|
||||
class={{value-entered this.accountPassword}}
|
||||
/>
|
||||
<label class="alt-placeholder" for="new-account-password">
|
||||
{{i18n "user.password.title"}}
|
||||
</label>
|
||||
|
||||
<div class="create-account__password-info">
|
||||
<div class="create-account__password-tip-validation">
|
||||
<InputTip
|
||||
@validation={{this.passwordValidation}}
|
||||
id="password-validation"
|
||||
/>
|
||||
{{#unless this.passwordValidation.reason}}
|
||||
<span class="more-info" id="password-validation-more-info">
|
||||
{{this.passwordInstructions}}
|
||||
</span>
|
||||
{{/unless}}
|
||||
<div
|
||||
class={{concat-class
|
||||
"caps-lock-warning"
|
||||
(unless this.capsLockOn "hidden")
|
||||
}}
|
||||
>
|
||||
{{d-icon "triangle-exclamation"}}
|
||||
{{i18n "login.caps_lock_warning"}}
|
||||
</div>
|
||||
</div>
|
||||
<TogglePasswordMask
|
||||
@maskPassword={{this.maskPassword}}
|
||||
@togglePasswordMask={{this.togglePasswordMask}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="password-confirmation">
|
||||
<label for="new-account-password-confirmation">
|
||||
{{i18n "user.password_confirmation.title"}}
|
||||
</label>
|
||||
<HoneypotInput
|
||||
@id="new-account-confirmation"
|
||||
@autocomplete="new-password"
|
||||
@value={{this.accountHoneypot}}
|
||||
/>
|
||||
<Input
|
||||
@value={{this.accountChallenge}}
|
||||
id="new-account-challenge"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.requireInviteCode}}
|
||||
<div class="input-group create-account__invite-code">
|
||||
<Input
|
||||
@value={{this.inviteCode}}
|
||||
id="inviteCode"
|
||||
class={{value-entered this.inviteCode}}
|
||||
/>
|
||||
<label class="alt-placeholder" for="invite-code">
|
||||
{{i18n "user.invite_code.title"}}
|
||||
</label>
|
||||
<span class="more-info">
|
||||
{{i18n "user.invite_code.instructions"}}
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<PluginOutlet
|
||||
@name="create-account-after-password"
|
||||
@outletArgs={{hash
|
||||
accountName=this.accountName
|
||||
accountUsername=this.accountUsername
|
||||
accountPassword=this.accountPassword
|
||||
userFields=this.userFields
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
class={{concat-class
|
||||
"input-group"
|
||||
"create-account__fullname"
|
||||
(if this.siteSettings.full_name_required "required")
|
||||
}}
|
||||
>
|
||||
{{#if this.fullnameRequired}}
|
||||
<TextField
|
||||
@disabled={{this.nameDisabled}}
|
||||
@value={{this.accountName}}
|
||||
@id="new-account-name"
|
||||
aria-describedby="fullname-validation fullname-validation-more-info"
|
||||
aria-invalid={{this.nameValidation.failed}}
|
||||
class={{value-entered this.accountName}}
|
||||
/>
|
||||
<label class="alt-placeholder" for="new-account-name">
|
||||
{{i18n "user.name.title"}}
|
||||
</label>
|
||||
|
||||
<InputTip
|
||||
@validation={{this.nameValidation}}
|
||||
id="fullname-validation"
|
||||
/>
|
||||
{{#unless this.nameValidation.reason}}
|
||||
<span class="more-info" id="fullname-validation-more-info">
|
||||
{{this.nameInstructions}}
|
||||
</span>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if this.userFields}}
|
||||
<div class="user-fields">
|
||||
{{#each this.userFields as |f|}}
|
||||
<div class="input-group">
|
||||
<UserField
|
||||
@field={{f.field}}
|
||||
@value={{f.value}}
|
||||
@validation={{f.validation}}
|
||||
class={{value-entered f.value}}
|
||||
/>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<PluginOutlet
|
||||
@name="create-account-after-user-fields"
|
||||
@outletArgs={{hash
|
||||
accountName=this.accountName
|
||||
accountUsername=this.accountUsername
|
||||
accountPassword=this.accountPassword
|
||||
userFields=this.userFields
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
|
||||
{{#if this.site.desktopView}}
|
||||
<SignupPageCta
|
||||
@formSubmitted={{this.formSubmitted}}
|
||||
@hasAuthOptions={{this.hasAuthOptions}}
|
||||
@createAccount={{this.createAccount}}
|
||||
@submitDisabled={{this.submitDisabled}}
|
||||
@disclaimerHtml={{this.disclaimerHtml}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if this.skipConfirmation}}
|
||||
{{loading-spinner size="large"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if this.hasAtLeastOneLoginButton}}
|
||||
{{#if this.site.mobileView}}
|
||||
<div class="login-or-separator"><span>
|
||||
{{i18n "login.or"}}</span></div>{{/if}}
|
||||
<div class="login-right-side">
|
||||
<LoginButtons
|
||||
@externalLogin={{this.externalLogin}}
|
||||
@context="create-account"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if (and this.showCreateForm this.site.mobileView)}}
|
||||
<SignupPageCta
|
||||
@formSubmitted={{this.formSubmitted}}
|
||||
@hasAuthOptions={{this.hasAuthOptions}}
|
||||
@createAccount={{this.createAccount}}
|
||||
@submitDisabled={{this.submitDisabled}}
|
||||
@disclaimerHtml={{this.disclaimerHtml}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
@ -8,7 +8,8 @@ acceptance("Modal - Login", function () {
|
||||
chromeTest("You can tab to the login button", async function (assert) {
|
||||
await visit("/");
|
||||
await click("header .login-button");
|
||||
// you have to press the tab key twice to get to the login button
|
||||
// you have to press the tab key thrice to get to the login button
|
||||
await tab({ unRestrainTabIndex: true });
|
||||
await tab({ unRestrainTabIndex: true });
|
||||
await tab({ unRestrainTabIndex: true });
|
||||
assert.dom(".d-modal__footer #login-button").isFocused();
|
||||
|
@ -0,0 +1,62 @@
|
||||
import { render } from "@ember/test-helpers";
|
||||
import { module, test } from "qunit";
|
||||
import FlashMessage from "discourse/components/flash-message";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
|
||||
module("Integration | Component | flash-message", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("it renders the correct class for a success flash", async function (assert) {
|
||||
const flash = "Success message";
|
||||
const type = "success";
|
||||
|
||||
await render(<template>
|
||||
<FlashMessage @flash={{flash}} @type={{type}} />
|
||||
</template>);
|
||||
|
||||
assert.dom(".alert.alert-success").hasText(flash);
|
||||
assert.dom(".alert").hasClass("alert-success");
|
||||
});
|
||||
|
||||
test("it renders the correct class for an error flash", async function (assert) {
|
||||
const flash = "Error message";
|
||||
const type = "error";
|
||||
|
||||
await render(<template>
|
||||
<FlashMessage @flash={{flash}} @type={{type}} />
|
||||
</template>);
|
||||
|
||||
assert.dom(".alert.alert-error").hasText(flash);
|
||||
assert.dom(".alert").hasClass("alert-error");
|
||||
});
|
||||
|
||||
test("it renders the correct class for a warning flash", async function (assert) {
|
||||
const flash = "Warning message";
|
||||
const type = "warning";
|
||||
|
||||
await render(<template>
|
||||
<FlashMessage @flash={{flash}} @type={{type}} />
|
||||
</template>);
|
||||
|
||||
assert.dom(".alert.alert-warning").hasText(flash);
|
||||
assert.dom(".alert").hasClass("alert-warning");
|
||||
});
|
||||
|
||||
test("it renders the correct class for an info flash", async function (assert) {
|
||||
const flash = "Info message";
|
||||
const type = "info";
|
||||
|
||||
await render(<template>
|
||||
<FlashMessage @flash={{flash}} @type={{type}} />
|
||||
</template>);
|
||||
|
||||
assert.dom(".alert.alert-info").hasText(flash);
|
||||
assert.dom(".alert").hasClass("alert-info");
|
||||
});
|
||||
|
||||
test("it does not render anything when flash is not provided", async function (assert) {
|
||||
await render(<template><FlashMessage /></template>);
|
||||
|
||||
assert.dom(".alert").doesNotExist();
|
||||
});
|
||||
});
|
@ -54,6 +54,7 @@
|
||||
@import "sidebar-more-section-links";
|
||||
@import "sidebar-section-link";
|
||||
@import "static-login";
|
||||
@import "login-signup-page";
|
||||
@import "tagging";
|
||||
@import "tooltip";
|
||||
@import "topic-admin-menu";
|
||||
|
306
app/assets/stylesheets/common/base/login-signup-page.scss
Normal file
306
app/assets/stylesheets/common/base/login-signup-page.scss
Normal file
@ -0,0 +1,306 @@
|
||||
// Shared styles
|
||||
|
||||
.login-fullpage,
|
||||
.signup-fullpage {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.login-fullpage,
|
||||
.signup-fullpage,
|
||||
.invites-show {
|
||||
.signup-body,
|
||||
.login-body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.login-page-cta,
|
||||
.signup-page-cta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__existing-account,
|
||||
&__no-account-yet {
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
|
||||
&__disclaimer {
|
||||
color: var(--primary-medium);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.login-left-side {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 3rem;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.login-right-side {
|
||||
padding: 3rem;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 1px;
|
||||
height: 80%;
|
||||
transform: translateY(-50%);
|
||||
background-color: var(--primary-low);
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 0.8em;
|
||||
|
||||
input,
|
||||
.select-kit-header {
|
||||
padding: 0.75em 0.77em;
|
||||
min-width: 250px;
|
||||
margin-bottom: 0.25em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border: 1px solid var(--tertiary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--tertiary-rgb), 0.25);
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
background-color: var(--primary-low);
|
||||
}
|
||||
|
||||
span.more-info {
|
||||
color: var(--primary-medium);
|
||||
min-height: 1.4em; // prevents height increase due to tips
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
label.alt-placeholder,
|
||||
.user-field.text label.control-label,
|
||||
.user-field.dropdown label.control-label,
|
||||
.user-field.multiselect label.control-label {
|
||||
color: var(--primary-medium);
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
left: 1em;
|
||||
top: 10px;
|
||||
box-shadow: 0 0 0 0px rgba(var(--tertiary-rgb), 0);
|
||||
transition: 0.2s ease all;
|
||||
}
|
||||
|
||||
.user-field.text label.control-label,
|
||||
.user-field.dropdown label.control-label,
|
||||
.user-field.multiselect label.control-label {
|
||||
z-index: 999;
|
||||
top: -8px;
|
||||
left: calc(1em - 0.25em);
|
||||
background-color: var(--secondary);
|
||||
padding: 0 0.25em 0 0.25em;
|
||||
font-size: $font-down-1;
|
||||
}
|
||||
|
||||
.user-field.text:focus-within,
|
||||
.user-field.dropdown:focus-within,
|
||||
.user-field.multiselect:focus-within {
|
||||
z-index: 1000; // ensures the active input is always on top of sibling input labels
|
||||
}
|
||||
|
||||
input:focus + label.alt-placeholder,
|
||||
input.value-entered + label.alt-placeholder {
|
||||
top: -8px;
|
||||
left: calc(1em - 0.25em);
|
||||
background-color: var(--secondary);
|
||||
padding: 0 0.25em 0 0.25em;
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
|
||||
input.alt-placeholder:invalid {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
#email-login-link {
|
||||
transition: opacity 0.5s;
|
||||
|
||||
&.no-login-filled {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
#email-login-link,
|
||||
.login__password-links {
|
||||
font-size: var(--font-down-1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tip:not(:empty) + label.more-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#login-form {
|
||||
margin-block: 2em 1.2em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.input-group {
|
||||
&.create-account-email,
|
||||
&.create-account__username,
|
||||
&.create-account__fullname.required {
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.create-account-associate-link {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#login-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
white-space: nowrap;
|
||||
|
||||
.btn-social {
|
||||
border: 1px solid var(--primary-low);
|
||||
}
|
||||
}
|
||||
|
||||
.login-welcome-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-social-title {
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.tip {
|
||||
&.bad {
|
||||
color: var(--danger);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.more-info,
|
||||
.instructions {
|
||||
color: var(--primary-medium);
|
||||
min-height: 1.4em;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.caps-lock-warning {
|
||||
color: var(--danger);
|
||||
font-size: var(--font-down-1);
|
||||
font-weight: bold;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.create-account__password-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.inline-spinner {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
// Login page
|
||||
|
||||
.login-fullpage {
|
||||
#second-factor {
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 0.75em 0.5em;
|
||||
min-width: 250px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border: 1px solid var(--tertiary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--tertiary-rgb), 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Signup page
|
||||
|
||||
.signup-fullpage {
|
||||
.password-confirmation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-fields .input-group {
|
||||
.user-field {
|
||||
&.text {
|
||||
&.value-entered label.alt-placeholder.control-label,
|
||||
input:focus + label.alt-placeholder.control-label {
|
||||
top: -8px;
|
||||
left: calc(1em - 0.25em);
|
||||
background-color: var(--secondary);
|
||||
padding: 0 0.25em 0 0.25em;
|
||||
font-size: 14px;
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
|
||||
label.alt-placeholder.control-label {
|
||||
color: var(--primary-medium);
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 12px;
|
||||
transition: 0.2s ease all;
|
||||
max-width: calc(100% - 2em);
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
details:not(.has-selection) span.name,
|
||||
details:not(.has-selection) span.formatted-selection {
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
|
||||
.select-kit-row span.name {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.select-kit.combo-box.is-expanded summary {
|
||||
outline: none;
|
||||
border: 1px solid var(--tertiary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--tertiary-rgb), 0.25);
|
||||
}
|
||||
|
||||
.controls .checkbox-label {
|
||||
input[type="checkbox"].ember-checkbox {
|
||||
width: 1em !important;
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -74,6 +74,9 @@
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
.create-account-associate-link {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
|
@ -12,3 +12,4 @@
|
||||
@import "post-action-feedback";
|
||||
@import "topic";
|
||||
@import "user";
|
||||
@import "login-signup-page";
|
||||
|
72
app/assets/stylesheets/desktop/login-signup-page.scss
Normal file
72
app/assets/stylesheets/desktop/login-signup-page.scss
Normal file
@ -0,0 +1,72 @@
|
||||
// Shared styles
|
||||
|
||||
.login-fullpage,
|
||||
.signup-fullpage,
|
||||
.invites-show {
|
||||
.login-page-cta,
|
||||
.signup-page-cta {
|
||||
&__buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
font-size: var(--font-0) !important;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&__existing-account,
|
||||
&__no-account-yet {
|
||||
font-size: var(--font-down);
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
&::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: var(--primary-low);
|
||||
margin-block: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#login-buttons {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
// important to maintain narrow desktop widths
|
||||
// for auth modals in Discourse Hub on iPad
|
||||
.has-alt-auth {
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
gap: 0;
|
||||
.login-page-cta,
|
||||
.signup-page-cta {
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
.login-left-side {
|
||||
overflow: unset;
|
||||
padding: 1em;
|
||||
max-width: unset;
|
||||
}
|
||||
.login-right-side {
|
||||
padding: 1em;
|
||||
max-width: unset;
|
||||
&::before {
|
||||
height: 1px;
|
||||
width: calc(100% - 2em);
|
||||
left: 1em;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.signup-progress-bar {
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@
|
||||
@import "menu-panel";
|
||||
@import "list-controls";
|
||||
@import "login-modal";
|
||||
@import "login-signup-page";
|
||||
@import "modal";
|
||||
@import "modal-overrides";
|
||||
@import "new-user";
|
||||
|
@ -27,6 +27,7 @@
|
||||
.login-right-side {
|
||||
padding: 1rem 0 0;
|
||||
background: unset;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.login-or-separator {
|
||||
|
170
app/assets/stylesheets/mobile/login-signup-page.scss
Normal file
170
app/assets/stylesheets/mobile/login-signup-page.scss
Normal file
@ -0,0 +1,170 @@
|
||||
// Shared styles
|
||||
|
||||
.login-fullpage,
|
||||
.signup-fullpage,
|
||||
.invites-show {
|
||||
.signup-body,
|
||||
.login-body {
|
||||
flex-direction: column;
|
||||
gap: unset;
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.login-page-cta,
|
||||
.signup-page-cta {
|
||||
font-size: var(--font-down-1);
|
||||
padding: 1rem;
|
||||
padding-top: 0;
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
#login-form,
|
||||
.login-form {
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
|
||||
.input-group {
|
||||
input {
|
||||
height: 2.5em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
margin-bottom: 1em;
|
||||
|
||||
.user-field:not(.dropdown) label.alt-placeholder {
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
input:focus + label,
|
||||
input.value-entered + label.alt-placeholder {
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
input.alt-placeholder:invalid {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
label.more-info {
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#login-buttons {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 1rem;
|
||||
gap: 0.25em;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
padding-block: 0.65rem;
|
||||
border: 1px solid var(--primary-low);
|
||||
flex: 1 1 calc(50% - 0.25em);
|
||||
font-size: var(--font-down-1);
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.caps-lock-warning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-welcome-header {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.login-right-side {
|
||||
padding: 1rem 0 0;
|
||||
background: unset;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.login-or-separator {
|
||||
border-top: 1px solid var(--primary-low);
|
||||
position: relative;
|
||||
margin-block: 1rem;
|
||||
|
||||
span {
|
||||
transform: translate(-50%, -50%);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
background: var(--secondary);
|
||||
padding-inline: 0.5rem;
|
||||
color: var(--primary-medium);
|
||||
font-size: var(--font-down-1-rem);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Login page
|
||||
|
||||
.login-fullpage .login-body {
|
||||
.login-page-cta {
|
||||
&__buttons {
|
||||
.login-page-cta__login {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
&__signup {
|
||||
background: none !important;
|
||||
font-size: var(--font-down);
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Signup page
|
||||
|
||||
.signup-fullpage .signup-body {
|
||||
.signup-page-cta {
|
||||
&__buttons {
|
||||
.signup-page-cta__signup {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
&__login {
|
||||
background: none !important;
|
||||
font-size: var(--font-down);
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.login-right-side::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-welcome-header {
|
||||
padding-bottom: 0.25rem;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.signup-progress-bar {
|
||||
padding-inline: 1em;
|
||||
}
|
||||
|
||||
#login-form {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
@ -2337,6 +2337,7 @@ en:
|
||||
associate: "Already have an account? <a href='%{associate_link}'>Log In</a> to link your %{provider} account."
|
||||
activation_title: "Activate your account"
|
||||
already_have_account: "Already have an account?"
|
||||
no_account_yet: "Don't have an account?"
|
||||
progress_bar:
|
||||
signup: "Sign Up"
|
||||
activate: "Activate"
|
||||
@ -2378,7 +2379,7 @@ en:
|
||||
login:
|
||||
header_title: "Welcome back"
|
||||
subheader_title: "Log in to your account"
|
||||
title: "Log in"
|
||||
title: "Log In"
|
||||
username: "User"
|
||||
password: "Password"
|
||||
show_password: "Show"
|
||||
|
@ -1871,6 +1871,7 @@ en:
|
||||
pending_users_reminder_delay_minutes: "Notify moderators if new users have been waiting for approval for longer than this many minutes. Set to -1 to disable notifications."
|
||||
persistent_sessions: "Users will remain logged in when the web browser is closed"
|
||||
maximum_session_age: "User will remain logged in for n hours since last visit"
|
||||
experimental_full_page_login: "EXPERIMENTAL: Replace the login/signup modal with a full page login/signup form."
|
||||
ga_version: "Version of Google Universal Analytics to use: v3 (analytics.js), v4 (gtag)"
|
||||
ga_universal_tracking_code: "Google Universal Analytics tracking code ID, eg: UA-12345678-9; see <a href='https://google.com/analytics' target='_blank'>https://google.com/analytics</a>"
|
||||
ga_universal_domain_name: "Google Universal Analytics domain name, eg: mysite.com; see <a href='https://google.com/analytics' target='_blank'>https://google.com/analytics</a>"
|
||||
|
@ -619,6 +619,11 @@ login:
|
||||
default: 1440
|
||||
min: 1
|
||||
max: 175200
|
||||
experimental_full_page_login:
|
||||
default: false
|
||||
client: true
|
||||
hidden: true
|
||||
|
||||
users:
|
||||
min_username_length:
|
||||
client: true
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
require "rotp"
|
||||
|
||||
shared_examples "login scenarios" do
|
||||
let(:login_modal) { PageObjects::Modals::Login.new }
|
||||
shared_examples "login scenarios" do |login_page_object|
|
||||
let(:login_form) { login_page_object }
|
||||
let(:activate_account) { PageObjects::Pages::ActivateAccount.new }
|
||||
let(:user_preferences_security_page) { PageObjects::Pages::UserPreferencesSecurity.new }
|
||||
fab!(:user) { Fabricate(:user, username: "john", password: "supersecurepassword") }
|
||||
@ -29,18 +29,14 @@ shared_examples "login scenarios" do
|
||||
it "can login" do
|
||||
EmailToken.confirm(Fabricate(:email_token, user: user).token)
|
||||
|
||||
login_modal.open
|
||||
login_modal.fill(username: "john", password: "supersecurepassword")
|
||||
login_modal.click_login
|
||||
login_form.open.fill(username: "john", password: "supersecurepassword").click_login
|
||||
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
||||
end
|
||||
|
||||
it "can login and activate account" do
|
||||
login_modal.open
|
||||
login_modal.fill(username: "john", password: "supersecurepassword")
|
||||
login_modal.click_login
|
||||
login_form.open.fill(username: "john", password: "supersecurepassword").click_login
|
||||
expect(page).to have_css(".not-activated-modal")
|
||||
login_modal.click(".activation-controls button.resend")
|
||||
login_form.click(".activation-controls button.resend")
|
||||
|
||||
activation_link = wait_for_email_link(user, :activation)
|
||||
visit activation_link
|
||||
@ -53,11 +49,9 @@ shared_examples "login scenarios" do
|
||||
end
|
||||
|
||||
it "redirects to the wizard after activating account" do
|
||||
login_modal.open
|
||||
login_modal.fill(username: "admin", password: "supersecurepassword")
|
||||
login_modal.click_login
|
||||
login_form.open.fill(username: "admin", password: "supersecurepassword").click_login
|
||||
expect(page).to have_css(".not-activated-modal")
|
||||
login_modal.click(".activation-controls button.resend")
|
||||
login_form.click(".activation-controls button.resend")
|
||||
|
||||
activation_link = wait_for_email_link(admin, :activation)
|
||||
visit activation_link
|
||||
@ -67,9 +61,7 @@ shared_examples "login scenarios" do
|
||||
end
|
||||
|
||||
it "shows error when when activation link is invalid" do
|
||||
login_modal.open
|
||||
login_modal.fill(username: "john", password: "supersecurepassword")
|
||||
login_modal.click_login
|
||||
login_form.open.fill(username: "john", password: "supersecurepassword").click_login
|
||||
expect(page).to have_css(".not-activated-modal")
|
||||
|
||||
visit "/u/activate-account/invalid"
|
||||
@ -83,15 +75,22 @@ shared_examples "login scenarios" do
|
||||
user.update!(password:)
|
||||
UserPasswordExpirer.expire_user_password(user)
|
||||
|
||||
login_modal.open
|
||||
login_modal.fill(username: user.username, password:)
|
||||
login_modal.click_login
|
||||
login_form.open.fill(username: user.username, password:).click_login
|
||||
|
||||
expect(login_modal.find("#modal-alert")).to have_content(
|
||||
expect(find(".alert-error")).to have_content(
|
||||
I18n.t("js.login.password_expired", reset_url: "/password-reset").gsub(/<.*?>/, ""),
|
||||
)
|
||||
|
||||
login_modal.find("#modal-alert a").click
|
||||
find(".alert-error a").click
|
||||
|
||||
# TODO: prefill username when fullpage
|
||||
if find("#username-or-email").value.blank?
|
||||
if page.has_css?("html.mobile-view", wait: 0)
|
||||
expect(page).to have_no_css(".d-modal.is-animating")
|
||||
end
|
||||
find("#username-or-email").fill_in(with: user.username)
|
||||
end
|
||||
|
||||
find("button.forgot-password-reset").click
|
||||
|
||||
reset_password_link = wait_for_email_link(user, :reset_password)
|
||||
@ -101,9 +100,7 @@ shared_examples "login scenarios" do
|
||||
|
||||
context "with login link" do
|
||||
it "can login" do
|
||||
login_modal.open
|
||||
login_modal.fill_username("john")
|
||||
login_modal.email_login_link
|
||||
login_form.open.fill_username("john").email_login_link
|
||||
|
||||
login_link = wait_for_email_link(user, :email_login)
|
||||
visit login_link
|
||||
@ -113,6 +110,26 @@ shared_examples "login scenarios" do
|
||||
end
|
||||
end
|
||||
|
||||
context "when login is required" do
|
||||
before { SiteSetting.login_required = true }
|
||||
|
||||
it "cannot browse annonymously" do
|
||||
visit "/"
|
||||
|
||||
if SiteSetting.experimental_full_page_login
|
||||
expect(page).to have_css(".login-fullpage")
|
||||
else
|
||||
expect(page).to have_css(".login-welcome")
|
||||
expect(page).to have_css(".site-logo")
|
||||
find(".login-welcome .login-button").click
|
||||
end
|
||||
|
||||
EmailToken.confirm(Fabricate(:email_token, user: user).token)
|
||||
login_form.fill(username: "john", password: "supersecurepassword").click_login
|
||||
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
||||
end
|
||||
end
|
||||
|
||||
context "with two-factor authentication" do
|
||||
let!(:user_second_factor) { Fabricate(:user_second_factor_totp, user: user) }
|
||||
let!(:user_second_factor_backup) { Fabricate(:user_second_factor_backup, user: user) }
|
||||
@ -127,93 +144,82 @@ shared_examples "login scenarios" do
|
||||
before { SiteSetting.enforce_second_factor = "all" }
|
||||
|
||||
it "requires to set 2FA after login" do
|
||||
login_modal.open
|
||||
login_modal.fill(username: "jane", password: "supersecurepassword")
|
||||
login_modal.click_login
|
||||
login_form.open.fill(username: "jane", password: "supersecurepassword").click_login
|
||||
|
||||
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
||||
expect(page).to have_content(I18n.t("js.user.second_factor.enforced_notice"))
|
||||
end
|
||||
end
|
||||
|
||||
it "can login with totp" do
|
||||
login_modal.open
|
||||
login_modal.fill(username: "john", password: "supersecurepassword")
|
||||
login_modal.click_login
|
||||
expect(page).to have_css(".login-modal-body.second-factor")
|
||||
login_form.open.fill(username: "john", password: "supersecurepassword").click_login
|
||||
|
||||
expect(page).to have_css(".second-factor")
|
||||
|
||||
totp = ROTP::TOTP.new(user_second_factor.data).now
|
||||
find("#login-second-factor").fill_in(with: totp)
|
||||
login_modal.click_login
|
||||
login_form.click_login
|
||||
|
||||
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
||||
end
|
||||
|
||||
it "can login with backup code" do
|
||||
login_modal.open
|
||||
login_modal.fill(username: "john", password: "supersecurepassword")
|
||||
login_modal.click_login
|
||||
expect(page).to have_css(".login-modal-body.second-factor")
|
||||
login_form.open.fill(username: "john", password: "supersecurepassword").click_login
|
||||
|
||||
expect(page).to have_css(".second-factor")
|
||||
|
||||
find(".toggle-second-factor-method").click
|
||||
find(".second-factor-token-input").fill_in(with: "iAmValidBackupCode")
|
||||
login_modal.click_login
|
||||
login_form.click_login
|
||||
|
||||
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
||||
end
|
||||
|
||||
it "can login with login link and totp" do
|
||||
login_modal.open
|
||||
login_modal.fill_username("john")
|
||||
login_modal.email_login_link
|
||||
login_form.open.fill_username("john").email_login_link
|
||||
|
||||
login_link = wait_for_email_link(user, :email_login)
|
||||
visit login_link
|
||||
|
||||
totp = ROTP::TOTP.new(user_second_factor.data).now
|
||||
find(".second-factor-token-input").fill_in(with: totp)
|
||||
find(".email-login-form .btn-primary").click
|
||||
|
||||
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
||||
end
|
||||
|
||||
it "can login with login link and backup code" do
|
||||
login_modal.open
|
||||
login_modal.fill_username("john")
|
||||
login_modal.email_login_link
|
||||
login_form.open.fill_username("john").email_login_link
|
||||
|
||||
login_link = wait_for_email_link(user, :email_login)
|
||||
visit login_link
|
||||
|
||||
find(".toggle-second-factor-method").click
|
||||
find(".second-factor-token-input").fill_in(with: "iAmValidBackupCode")
|
||||
find(".email-login-form .btn-primary").click
|
||||
|
||||
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
||||
end
|
||||
|
||||
it "can reset password with TOTP" do
|
||||
login_modal.open
|
||||
login_modal.fill_username("john")
|
||||
login_modal.forgot_password
|
||||
login_form.open.fill_username("john").forgot_password
|
||||
find("button.forgot-password-reset").click
|
||||
|
||||
reset_password_link = wait_for_email_link(user, :reset_password)
|
||||
visit reset_password_link
|
||||
|
||||
totp = ROTP::TOTP.new(user_second_factor.data).now
|
||||
find(".second-factor-token-input").fill_in(with: totp)
|
||||
find(".password-reset .btn-primary").click
|
||||
|
||||
find("#new-account-password").fill_in(with: "newsuperpassword")
|
||||
find(".change-password-form .btn-primary").click
|
||||
|
||||
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
||||
end
|
||||
|
||||
it "shows error correctly when TOTP code is invalid" do
|
||||
login_modal.open
|
||||
login_modal.fill_username("john")
|
||||
login_modal.forgot_password
|
||||
login_form.open.fill_username("john").forgot_password
|
||||
find("button.forgot-password-reset").click
|
||||
|
||||
reset_password_link = wait_for_email_link(user, :reset_password)
|
||||
visit reset_password_link
|
||||
|
||||
find(".second-factor-token-input").fill_in(with: "123456")
|
||||
find(".password-reset .btn-primary").click
|
||||
|
||||
@ -221,25 +227,21 @@ shared_examples "login scenarios" do
|
||||
".alert-error",
|
||||
text: "Invalid authentication code. Each code can only be used once.",
|
||||
)
|
||||
|
||||
expect(page).to have_css(".second-factor-token-input")
|
||||
end
|
||||
|
||||
it "can reset password with a backup code" do
|
||||
login_modal.open
|
||||
login_modal.fill_username("john")
|
||||
login_modal.forgot_password
|
||||
login_form.open.fill_username("john").forgot_password
|
||||
find("button.forgot-password-reset").click
|
||||
|
||||
reset_password_link = wait_for_email_link(user, :reset_password)
|
||||
visit reset_password_link
|
||||
|
||||
find(".toggle-second-factor-method").click
|
||||
find(".second-factor-token-input").fill_in(with: "iAmValidBackupCode")
|
||||
find(".password-reset .btn-primary").click
|
||||
|
||||
find("#new-account-password").fill_in(with: "newsuperpassword")
|
||||
find(".change-password-form .btn-primary").click
|
||||
|
||||
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
||||
end
|
||||
end
|
||||
@ -247,10 +249,20 @@ end
|
||||
|
||||
describe "Login", type: :system do
|
||||
context "when desktop" do
|
||||
include_examples "login scenarios"
|
||||
include_examples "login scenarios", PageObjects::Modals::Login.new
|
||||
end
|
||||
|
||||
context "when mobile", mobile: true do
|
||||
include_examples "login scenarios"
|
||||
include_examples "login scenarios", PageObjects::Modals::Login.new
|
||||
end
|
||||
|
||||
context "when fullpage desktop" do
|
||||
before { SiteSetting.experimental_full_page_login = true }
|
||||
include_examples "login scenarios", PageObjects::Pages::Login.new
|
||||
end
|
||||
|
||||
context "when fullpage mobile", mobile: true do
|
||||
before { SiteSetting.experimental_full_page_login = true }
|
||||
include_examples "login scenarios", PageObjects::Pages::Login.new
|
||||
end
|
||||
end
|
||||
|
@ -13,6 +13,7 @@ module PageObjects
|
||||
|
||||
def open
|
||||
visit("/login")
|
||||
self
|
||||
end
|
||||
|
||||
def open_from_header
|
||||
@ -47,19 +48,23 @@ module PageObjects
|
||||
expect(page).to have_css(".d-modal:not(.is-animating)")
|
||||
end
|
||||
find(selector).fill_in(with: text)
|
||||
self
|
||||
end
|
||||
|
||||
def fill_username(username)
|
||||
fill_input("#login-account-name", username)
|
||||
self
|
||||
end
|
||||
|
||||
def fill_password(password)
|
||||
fill_input("#login-account-password", password)
|
||||
self
|
||||
end
|
||||
|
||||
def fill(username: nil, password: nil)
|
||||
fill_username(username) if username
|
||||
fill_password(password) if password
|
||||
self
|
||||
end
|
||||
|
||||
def click_social_button(provider)
|
||||
|
@ -13,6 +13,7 @@ module PageObjects
|
||||
|
||||
def open
|
||||
visit("/signup")
|
||||
self
|
||||
end
|
||||
|
||||
def open_from_header
|
||||
@ -51,26 +52,32 @@ module PageObjects
|
||||
|
||||
def fill_email(email)
|
||||
fill_input("#new-account-email", email)
|
||||
self
|
||||
end
|
||||
|
||||
def fill_username(username)
|
||||
fill_input("#new-account-username", username)
|
||||
self
|
||||
end
|
||||
|
||||
def fill_name(name)
|
||||
fill_input("#new-account-name", name)
|
||||
self
|
||||
end
|
||||
|
||||
def fill_password(password)
|
||||
fill_input("#new-account-password", password)
|
||||
self
|
||||
end
|
||||
|
||||
def fill_code(code)
|
||||
fill_input("#inviteCode", code)
|
||||
self
|
||||
end
|
||||
|
||||
def fill_custom_field(name, value)
|
||||
find(".user-field-#{name.downcase} input").fill_in(with: value)
|
||||
self
|
||||
end
|
||||
|
||||
def has_valid_email?
|
||||
|
74
spec/system/page_objects/pages/login.rb
Normal file
74
spec/system/page_objects/pages/login.rb
Normal file
@ -0,0 +1,74 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class Login < PageObjects::Pages::Base
|
||||
def open?
|
||||
has_css?(".login-fullpage")
|
||||
end
|
||||
|
||||
def closed?
|
||||
has_no_css?(".login-fullpage")
|
||||
end
|
||||
|
||||
def open
|
||||
visit("/login")
|
||||
self
|
||||
end
|
||||
|
||||
def open_from_header
|
||||
find(".login-button").click
|
||||
end
|
||||
|
||||
def click(selector)
|
||||
if page.has_css?("html.mobile-view", wait: 0)
|
||||
expect(page).to have_no_css(".d-modal.is-animating")
|
||||
end
|
||||
find(selector).click
|
||||
end
|
||||
|
||||
def open_signup
|
||||
click("#new-account-link")
|
||||
end
|
||||
|
||||
def click_login
|
||||
click("#login-button")
|
||||
end
|
||||
|
||||
def email_login_link
|
||||
click("#email-login-link")
|
||||
end
|
||||
|
||||
def forgot_password
|
||||
click("#forgot-password-link")
|
||||
end
|
||||
|
||||
def fill_input(selector, text)
|
||||
if page.has_css?("html.mobile-view", wait: 0)
|
||||
expect(page).to have_no_css(".d-modal.is-animating")
|
||||
end
|
||||
find(selector).fill_in(with: text)
|
||||
end
|
||||
|
||||
def fill_username(username)
|
||||
fill_input("#login-account-name", username)
|
||||
self
|
||||
end
|
||||
|
||||
def fill_password(password)
|
||||
fill_input("#login-account-password", password)
|
||||
self
|
||||
end
|
||||
|
||||
def fill(username: nil, password: nil)
|
||||
fill_username(username) if username
|
||||
fill_password(password) if password
|
||||
self
|
||||
end
|
||||
|
||||
def click_social_button(provider)
|
||||
click(".btn-social.#{provider}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
106
spec/system/page_objects/pages/signup.rb
Normal file
106
spec/system/page_objects/pages/signup.rb
Normal file
@ -0,0 +1,106 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class Signup < PageObjects::Pages::Base
|
||||
def open?
|
||||
has_css?(".signup-fullpage")
|
||||
end
|
||||
|
||||
def closed?
|
||||
has_no_css?(".signup-fullpage")
|
||||
end
|
||||
|
||||
def open
|
||||
visit("/signup")
|
||||
self
|
||||
end
|
||||
|
||||
def open_from_header
|
||||
find(".sign-up-button").click
|
||||
end
|
||||
|
||||
def click(selector)
|
||||
if page.has_css?("html.mobile-view", wait: 0)
|
||||
expect(page).to have_no_css(".d-modal.is-animating")
|
||||
end
|
||||
find(selector).click
|
||||
end
|
||||
|
||||
def open_login
|
||||
click("#login-link")
|
||||
end
|
||||
|
||||
def click_create_account
|
||||
click(".signup-fullpage .btn-primary")
|
||||
end
|
||||
|
||||
def has_password_input?
|
||||
has_css?("#new-account-password")
|
||||
end
|
||||
|
||||
def has_no_password_input?
|
||||
has_no_css?("#new-account-password")
|
||||
end
|
||||
|
||||
def fill_input(selector, text)
|
||||
if page.has_css?("html.mobile-view", wait: 0)
|
||||
expect(page).to have_no_css(".d-modal.is-animating")
|
||||
end
|
||||
find(selector).fill_in(with: text)
|
||||
end
|
||||
|
||||
def fill_email(email)
|
||||
fill_input("#new-account-email", email)
|
||||
self
|
||||
end
|
||||
|
||||
def fill_username(username)
|
||||
fill_input("#new-account-username", username)
|
||||
self
|
||||
end
|
||||
|
||||
def fill_name(name)
|
||||
fill_input("#new-account-name", name)
|
||||
self
|
||||
end
|
||||
|
||||
def fill_password(password)
|
||||
fill_input("#new-account-password", password)
|
||||
self
|
||||
end
|
||||
|
||||
def fill_code(code)
|
||||
fill_input("#inviteCode", code)
|
||||
self
|
||||
end
|
||||
|
||||
def fill_custom_field(name, value)
|
||||
find(".user-field-#{name.downcase} input").fill_in(with: value)
|
||||
self
|
||||
end
|
||||
|
||||
def has_valid_email?
|
||||
find(".create-account-email").has_css?("#account-email-validation.good")
|
||||
end
|
||||
|
||||
def has_valid_username?
|
||||
find(".create-account__username").has_css?("#username-validation.good")
|
||||
end
|
||||
|
||||
def has_valid_password?
|
||||
find(".create-account__password").has_css?("#password-validation.good")
|
||||
end
|
||||
|
||||
def has_valid_fields?
|
||||
has_valid_email?
|
||||
has_valid_username?
|
||||
has_valid_password?
|
||||
end
|
||||
|
||||
def click_social_button(provider)
|
||||
click(".btn-social.#{provider}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,8 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
shared_examples "signup scenarios" do
|
||||
let(:login_modal) { PageObjects::Modals::Login.new }
|
||||
let(:signup_modal) { PageObjects::Modals::Signup.new }
|
||||
shared_examples "signup scenarios" do |signup_page_object, login_page_object|
|
||||
let(:login_form) { login_page_object }
|
||||
let(:signup_form) { signup_page_object }
|
||||
let(:invite_form) { PageObjects::Pages::InviteForm.new }
|
||||
let(:activate_account) { PageObjects::Pages::ActivateAccount.new }
|
||||
let(:invite) { Fabricate(:invite, email: "johndoe@example.com") }
|
||||
@ -12,24 +12,26 @@ shared_examples "signup scenarios" do
|
||||
before { Jobs.run_immediately! }
|
||||
|
||||
it "can signup" do
|
||||
signup_modal.open
|
||||
signup_modal.fill_email("johndoe@example.com")
|
||||
signup_modal.fill_username("john")
|
||||
signup_modal.fill_password("supersecurepassword")
|
||||
expect(signup_modal).to have_valid_fields
|
||||
signup_form
|
||||
.open
|
||||
.fill_email("johndoe@example.com")
|
||||
.fill_username("john")
|
||||
.fill_password("supersecurepassword")
|
||||
expect(signup_form).to have_valid_fields
|
||||
|
||||
signup_modal.click_create_account
|
||||
signup_form.click_create_account
|
||||
expect(page).to have_css(".account-created")
|
||||
end
|
||||
|
||||
it "can signup and activate account" do
|
||||
signup_modal.open
|
||||
signup_modal.fill_email("johndoe@example.com")
|
||||
signup_modal.fill_username("john")
|
||||
signup_modal.fill_password("supersecurepassword")
|
||||
expect(signup_modal).to have_valid_fields
|
||||
signup_form
|
||||
.open
|
||||
.fill_email("johndoe@example.com")
|
||||
.fill_username("john")
|
||||
.fill_password("supersecurepassword")
|
||||
expect(signup_form).to have_valid_fields
|
||||
|
||||
signup_modal.click_create_account
|
||||
signup_form.click_create_account
|
||||
expect(page).to have_css(".account-created")
|
||||
|
||||
mail = ActionMailer::Base.deliveries.first
|
||||
@ -73,28 +75,30 @@ shared_examples "signup scenarios" do
|
||||
before { SiteSetting.invite_code = "cupcake" }
|
||||
|
||||
it "can signup with valid code" do
|
||||
signup_modal.open
|
||||
signup_modal.fill_email("johndoe@example.com")
|
||||
signup_modal.fill_username("john")
|
||||
signup_modal.fill_password("supersecurepassword")
|
||||
signup_modal.fill_code("cupcake")
|
||||
expect(signup_modal).to have_valid_fields
|
||||
signup_form
|
||||
.open
|
||||
.fill_email("johndoe@example.com")
|
||||
.fill_username("john")
|
||||
.fill_password("supersecurepassword")
|
||||
.fill_code("cupcake")
|
||||
expect(signup_form).to have_valid_fields
|
||||
|
||||
signup_modal.click_create_account
|
||||
signup_form.click_create_account
|
||||
expect(page).to have_css(".account-created")
|
||||
end
|
||||
|
||||
it "cannot signup with invalid code" do
|
||||
signup_modal.open
|
||||
signup_modal.fill_email("johndoe@example.com")
|
||||
signup_modal.fill_username("john")
|
||||
signup_modal.fill_password("supersecurepassword")
|
||||
signup_modal.fill_code("pudding")
|
||||
expect(signup_modal).to have_valid_fields
|
||||
signup_form
|
||||
.open
|
||||
.fill_email("johndoe@example.com")
|
||||
.fill_username("john")
|
||||
.fill_password("supersecurepassword")
|
||||
.fill_code("pudding")
|
||||
expect(signup_form).to have_valid_fields
|
||||
|
||||
signup_modal.click_create_account
|
||||
expect(signup_modal).to have_content(I18n.t("login.wrong_invite_code"))
|
||||
expect(signup_modal).to have_no_css(".account-created")
|
||||
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")
|
||||
end
|
||||
end
|
||||
|
||||
@ -109,26 +113,27 @@ shared_examples "signup scenarios" do
|
||||
end
|
||||
|
||||
it "can signup when filling the custom field" do
|
||||
signup_modal.open
|
||||
signup_modal.fill_email("johndoe@example.com")
|
||||
signup_modal.fill_username("john")
|
||||
signup_modal.fill_password("supersecurepassword")
|
||||
signup_modal.fill_custom_field("Occupation", "Jedi")
|
||||
expect(signup_modal).to have_valid_fields
|
||||
signup_form
|
||||
.open
|
||||
.fill_email("johndoe@example.com")
|
||||
.fill_username("john")
|
||||
.fill_password("supersecurepassword")
|
||||
.fill_custom_field("Occupation", "Jedi")
|
||||
expect(signup_form).to have_valid_fields
|
||||
|
||||
signup_modal.click_create_account
|
||||
signup_form.click_create_account
|
||||
expect(page).to have_css(".account-created")
|
||||
end
|
||||
|
||||
it "cannot signup without filling the custom field" do
|
||||
signup_modal.open
|
||||
signup_modal.fill_email("johndoe@example.com")
|
||||
signup_modal.fill_username("john")
|
||||
signup_modal.fill_password("supersecurepassword")
|
||||
|
||||
signup_modal.click_create_account
|
||||
expect(signup_modal).to have_content(I18n.t("js.user_fields.required", name: "Occupation"))
|
||||
expect(signup_modal).to have_no_css(".account-created")
|
||||
signup_form
|
||||
.open
|
||||
.fill_email("johndoe@example.com")
|
||||
.fill_username("john")
|
||||
.fill_password("supersecurepassword")
|
||||
.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")
|
||||
end
|
||||
end
|
||||
|
||||
@ -139,47 +144,49 @@ shared_examples "signup scenarios" do
|
||||
end
|
||||
|
||||
it "can signup but cannot login until approval" do
|
||||
signup_modal.open
|
||||
signup_modal.fill_email("johndoe@example.com")
|
||||
signup_modal.fill_username("john")
|
||||
signup_modal.fill_password("supersecurepassword")
|
||||
expect(signup_modal).to have_valid_fields
|
||||
signup_modal.click_create_account
|
||||
signup_form
|
||||
.open
|
||||
.fill_email("johndoe@example.com")
|
||||
.fill_username("john")
|
||||
.fill_password("supersecurepassword")
|
||||
expect(signup_form).to have_valid_fields
|
||||
signup_form.click_create_account
|
||||
|
||||
wait_for(timeout: 5) { User.find_by(username: "john") != nil }
|
||||
|
||||
visit "/"
|
||||
login_modal.open
|
||||
login_modal.fill_username("john")
|
||||
login_modal.fill_password("supersecurepassword")
|
||||
login_modal.click_login
|
||||
expect(login_modal).to have_content(I18n.t("login.not_approved"))
|
||||
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"))
|
||||
|
||||
user = User.find_by(username: "john")
|
||||
user.update!(approved: true)
|
||||
EmailToken.confirm(Fabricate(:email_token, user: user).token)
|
||||
|
||||
login_modal.click_login
|
||||
login_form.click_login
|
||||
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
||||
end
|
||||
|
||||
it "can login directly when using an auto approved email" do
|
||||
signup_modal.open
|
||||
signup_modal.fill_email("johndoe@awesomeemail.com")
|
||||
signup_modal.fill_username("john")
|
||||
signup_modal.fill_password("supersecurepassword")
|
||||
expect(signup_modal).to have_valid_fields
|
||||
signup_modal.click_create_account
|
||||
signup_form
|
||||
.open
|
||||
.fill_email("johndoe@awesomeemail.com")
|
||||
.fill_username("john")
|
||||
.fill_password("supersecurepassword")
|
||||
expect(signup_form).to have_valid_fields
|
||||
signup_form.click_create_account
|
||||
|
||||
wait_for(timeout: 5) { User.find_by(username: "john") != nil }
|
||||
user = User.find_by(username: "john")
|
||||
EmailToken.confirm(Fabricate(:email_token, user: user).token)
|
||||
|
||||
visit "/"
|
||||
login_modal.open
|
||||
login_modal.fill_username("john")
|
||||
login_modal.fill_password("supersecurepassword")
|
||||
login_modal.click_login
|
||||
login_form.open
|
||||
login_form.fill_username("john")
|
||||
login_form.fill_password("supersecurepassword")
|
||||
login_form.click_login
|
||||
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
||||
end
|
||||
end
|
||||
@ -189,12 +196,13 @@ shared_examples "signup scenarios" do
|
||||
|
||||
it "can signup and activate account" do
|
||||
visit("/discuss/signup")
|
||||
signup_modal.fill_email("johndoe@example.com")
|
||||
signup_modal.fill_username("john")
|
||||
signup_modal.fill_password("supersecurepassword")
|
||||
expect(signup_modal).to have_valid_fields
|
||||
signup_form
|
||||
.fill_email("johndoe@example.com")
|
||||
.fill_username("john")
|
||||
.fill_password("supersecurepassword")
|
||||
expect(signup_form).to have_valid_fields
|
||||
|
||||
signup_modal.click_create_account
|
||||
signup_form.click_create_account
|
||||
expect(page).to have_css(".account-created")
|
||||
|
||||
mail = ActionMailer::Base.deliveries.first
|
||||
@ -216,13 +224,14 @@ shared_examples "signup scenarios" do
|
||||
before { SiteSetting.blocked_email_domains = "example.com" }
|
||||
|
||||
it "cannot signup" do
|
||||
signup_modal.open
|
||||
signup_modal.fill_email("johndoe@example.com")
|
||||
signup_modal.fill_username("john")
|
||||
signup_modal.fill_password("supersecurepassword")
|
||||
expect(signup_modal).to have_valid_username
|
||||
expect(signup_modal).to have_valid_password
|
||||
expect(signup_modal).to have_content(I18n.t("user.email.not_allowed"))
|
||||
signup_form
|
||||
.open
|
||||
.fill_email("johndoe@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"))
|
||||
end
|
||||
end
|
||||
|
||||
@ -230,12 +239,12 @@ shared_examples "signup scenarios" do
|
||||
before { SiteSetting.invite_only = true }
|
||||
|
||||
it "cannot open the signup modal" do
|
||||
signup_modal.open
|
||||
expect(signup_modal).to be_closed
|
||||
signup_form.open
|
||||
expect(signup_form).to be_closed
|
||||
expect(page).to have_no_css(".sign-up-button")
|
||||
|
||||
login_modal.open_from_header
|
||||
expect(login_modal).to have_no_css("#new-account-link")
|
||||
login_form.open_from_header
|
||||
expect(login_form).to have_no_css("#new-account-link")
|
||||
end
|
||||
|
||||
it "can signup with invite link" do
|
||||
@ -254,10 +263,28 @@ end
|
||||
|
||||
describe "Signup", type: :system do
|
||||
context "when desktop" do
|
||||
include_examples "signup scenarios"
|
||||
include_examples "signup scenarios",
|
||||
PageObjects::Modals::Signup.new,
|
||||
PageObjects::Modals::Login.new
|
||||
end
|
||||
|
||||
context "when mobile", mobile: true do
|
||||
include_examples "signup scenarios"
|
||||
include_examples "signup scenarios",
|
||||
PageObjects::Modals::Signup.new,
|
||||
PageObjects::Modals::Login.new
|
||||
end
|
||||
|
||||
context "when fullpage desktop" do
|
||||
before { SiteSetting.experimental_full_page_login = true }
|
||||
include_examples "signup scenarios",
|
||||
PageObjects::Pages::Signup.new,
|
||||
PageObjects::Pages::Login.new
|
||||
end
|
||||
|
||||
context "when fullpage mobile", mobile: true do
|
||||
before { SiteSetting.experimental_full_page_login = true }
|
||||
include_examples "signup scenarios",
|
||||
PageObjects::Pages::Signup.new,
|
||||
PageObjects::Pages::Login.new
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user