mirror of
https://github.com/discourse/discourse.git
synced 2025-10-03 17:21:20 +08:00
DEV: Remove full_page_login setting (#32189)
We are making this the only option for our login/signup pages on April 29th, 2025, per https://meta.discourse.org/t/introducing-our-new-fullscreen-signup-and-login-pages/340401. This commit removes the `full_page_login` setting and any logic around it, as well as deleting the old login and signup modals, and removing leftover problem checks and settings from the database.
This commit is contained in:
parent
19626bc75a
commit
ed1e0e30f2
36 changed files with 57 additions and 3075 deletions
|
@ -1,898 +0,0 @@
|
||||||
import { tracked } from "@glimmer/tracking";
|
|
||||||
import { A } from "@ember/array";
|
|
||||||
import Component, { Input } from "@ember/component";
|
|
||||||
import { hash } from "@ember/helper";
|
|
||||||
import { on } from "@ember/modifier";
|
|
||||||
import EmberObject, { action } from "@ember/object";
|
|
||||||
import { dependentKeyCompat } from "@ember/object/compat";
|
|
||||||
import { service } from "@ember/service";
|
|
||||||
import { htmlSafe } from "@ember/template";
|
|
||||||
import { isEmpty } from "@ember/utils";
|
|
||||||
import { observes } from "@ember-decorators/object";
|
|
||||||
import { Promise } from "rsvp";
|
|
||||||
import { and, not } from "truth-helpers";
|
|
||||||
import DModal from "discourse/components/d-modal";
|
|
||||||
import FullnameInput from "discourse/components/fullname-input";
|
|
||||||
import HoneypotInput from "discourse/components/honeypot-input";
|
|
||||||
import InputTip from "discourse/components/input-tip";
|
|
||||||
import LoginButtons from "discourse/components/login-buttons";
|
|
||||||
import PasswordField from "discourse/components/password-field";
|
|
||||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
|
||||||
import SignupPageCta from "discourse/components/signup-page-cta";
|
|
||||||
import SignupProgressBar from "discourse/components/signup-progress-bar";
|
|
||||||
import TogglePasswordMask from "discourse/components/toggle-password-mask";
|
|
||||||
import UserField from "discourse/components/user-field";
|
|
||||||
import WelcomeHeader from "discourse/components/welcome-header";
|
|
||||||
import concatClass from "discourse/helpers/concat-class";
|
|
||||||
import icon from "discourse/helpers/d-icon";
|
|
||||||
import loadingSpinner from "discourse/helpers/loading-spinner";
|
|
||||||
import routeAction from "discourse/helpers/route-action";
|
|
||||||
import valueEntered from "discourse/helpers/value-entered";
|
|
||||||
import { ajax } from "discourse/lib/ajax";
|
|
||||||
import { setting } from "discourse/lib/computed";
|
|
||||||
import cookie, { removeCookie } from "discourse/lib/cookie";
|
|
||||||
import discourseDebounce from "discourse/lib/debounce";
|
|
||||||
import discourseComputed, { bind } from "discourse/lib/decorators";
|
|
||||||
import NameValidationHelper from "discourse/lib/name-validation-helper";
|
|
||||||
import PasswordValidationHelper from "discourse/lib/password-validation-helper";
|
|
||||||
import { userPath } from "discourse/lib/url";
|
|
||||||
import UserFieldsValidationHelper from "discourse/lib/user-fields-validation-helper";
|
|
||||||
import UsernameValidationHelper from "discourse/lib/username-validation-helper";
|
|
||||||
import { emailValid } from "discourse/lib/utilities";
|
|
||||||
import { findAll } from "discourse/models/login-method";
|
|
||||||
import User from "discourse/models/user";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
export default class CreateAccount extends Component {
|
|
||||||
@service site;
|
|
||||||
@service siteSettings;
|
|
||||||
@service login;
|
|
||||||
|
|
||||||
@tracked isDeveloper = false;
|
|
||||||
@tracked accountName = this.model.accountName;
|
|
||||||
@tracked accountEmail = this.model.accountEmail;
|
|
||||||
@tracked accountUsername = this.model.accountUsername;
|
|
||||||
@tracked accountPassword = this.model.accountPassword;
|
|
||||||
@tracked authOptions = this.model.authOptions;
|
|
||||||
@tracked skipConfirmation = this.model.skipConfirmation;
|
|
||||||
accountChallenge = 0;
|
|
||||||
accountHoneypot = 0;
|
|
||||||
formSubmitted = false;
|
|
||||||
rejectedEmails = A();
|
|
||||||
prefilledUsername = null;
|
|
||||||
maskPassword = true;
|
|
||||||
emailValidationVisible = false;
|
|
||||||
nameValidationHelper = new NameValidationHelper(this);
|
|
||||||
usernameValidationHelper = new UsernameValidationHelper({
|
|
||||||
getAccountEmail: () => this.accountEmail,
|
|
||||||
getAccountUsername: () => this.accountUsername,
|
|
||||||
getPrefilledUsername: () => this.prefilledUsername,
|
|
||||||
getAuthOptionsUsername: () => this.authOptions?.username,
|
|
||||||
getForceValidationReason: () => this.forceValidationReason,
|
|
||||||
siteSettings: this.siteSettings,
|
|
||||||
isInvalid: () => this.isDestroying || this.isDestroyed,
|
|
||||||
updateIsDeveloper: (isDeveloper) => (this.isDeveloper = isDeveloper),
|
|
||||||
updateUsernames: (username) => {
|
|
||||||
this.accountUsername = username;
|
|
||||||
this.prefilledUsername = username;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
passwordValidationHelper = new PasswordValidationHelper(this);
|
|
||||||
userFieldsValidationHelper = new UserFieldsValidationHelper({
|
|
||||||
getUserFields: () => this.site.get("user_fields"),
|
|
||||||
getAccountPassword: () => this.accountPassword,
|
|
||||||
showValidationOnInit: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@setting("enable_local_logins") canCreateLocal;
|
|
||||||
@setting("require_invite_code") requireInviteCode;
|
|
||||||
|
|
||||||
init() {
|
|
||||||
super.init(...arguments);
|
|
||||||
|
|
||||||
if (cookie("email")) {
|
|
||||||
this.accountEmail = cookie("email");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fetchConfirmationValue();
|
|
||||||
|
|
||||||
if (this.model.skipConfirmation) {
|
|
||||||
this.performAccountCreation().finally(
|
|
||||||
() => (this.skipConfirmation = false)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@dependentKeyCompat
|
|
||||||
get userFields() {
|
|
||||||
return this.userFieldsValidationHelper.userFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
@dependentKeyCompat
|
|
||||||
get userFieldsValidation() {
|
|
||||||
return this.userFieldsValidationHelper.userFieldsValidation;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setAccountUsername(event) {
|
|
||||||
this.accountUsername = event.target.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@dependentKeyCompat
|
|
||||||
get usernameValidation() {
|
|
||||||
return this.usernameValidationHelper.usernameValidation;
|
|
||||||
}
|
|
||||||
|
|
||||||
get passwordValidation() {
|
|
||||||
return this.passwordValidationHelper.passwordValidation;
|
|
||||||
}
|
|
||||||
|
|
||||||
get nameTitle() {
|
|
||||||
return this.nameValidationHelper.nameTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
get nameValidation() {
|
|
||||||
return this.nameValidationHelper.nameValidation;
|
|
||||||
}
|
|
||||||
|
|
||||||
@dependentKeyCompat
|
|
||||||
get hasAuthOptions() {
|
|
||||||
return !isEmpty(this.authOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
@dependentKeyCompat
|
|
||||||
get forceValidationReason() {
|
|
||||||
return this.nameValidationHelper.forceValidationReason;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get showCreateForm() {
|
|
||||||
return (
|
|
||||||
(this.hasAuthOptions || this.canCreateLocal) && !this.skipConfirmation
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed("site.desktopView", "hasAuthOptions")
|
|
||||||
showExternalLoginButtons(desktopView, hasAuthOptions) {
|
|
||||||
return desktopView && !hasAuthOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed("formSubmitted")
|
|
||||||
submitDisabled() {
|
|
||||||
return this.formSubmitted;
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed("userFields", "hasAtLeastOneLoginButton", "hasAuthOptions")
|
|
||||||
modalBodyClasses(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(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
get usernameDisabled() {
|
|
||||||
return this.authOptions && !this.authOptions.can_edit_username;
|
|
||||||
}
|
|
||||||
|
|
||||||
get nameDisabled() {
|
|
||||||
return this.authOptions && !this.authOptions.can_edit_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed
|
|
||||||
showFullname() {
|
|
||||||
return this.site.full_name_visible_in_signup;
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed
|
|
||||||
fullnameRequired() {
|
|
||||||
return this.site.full_name_required_for_signup;
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed(
|
|
||||||
"emailValidation.ok",
|
|
||||||
"emailValidation.reason",
|
|
||||||
"emailValidationVisible"
|
|
||||||
)
|
|
||||||
showEmailValidation(
|
|
||||||
emailValidationOk,
|
|
||||||
emailValidationReason,
|
|
||||||
emailValidationVisible
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
emailValidationOk || (emailValidationReason && emailValidationVisible)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get showPasswordValidation() {
|
|
||||||
return this.passwordValidation.ok || this.passwordValidation.reason;
|
|
||||||
}
|
|
||||||
|
|
||||||
get showUsernameInstructions() {
|
|
||||||
return (
|
|
||||||
this.siteSettings.show_signup_form_username_instructions &&
|
|
||||||
!this.usernameValidation.reason
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get passwordRequired() {
|
|
||||||
return isEmpty(this.authOptions?.auth_provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
@discourseComputed
|
|
||||||
disclaimerHtml() {
|
|
||||||
if (this.site.tos_url && this.site.privacy_policy_url) {
|
|
||||||
return i18n("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("user.email.required"),
|
|
||||||
reason: forceValidationReason ? i18n("user.email.required") : null,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rejectedEmails.includes(email) || !emailValid(email)) {
|
|
||||||
return EmberObject.create(
|
|
||||||
Object.assign(failedAttrs, {
|
|
||||||
reason: i18n("user.email.invalid"),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.authOptions?.email === email && this.authOptions?.email_valid) {
|
|
||||||
return EmberObject.create({
|
|
||||||
ok: true,
|
|
||||||
reason: i18n("user.email.authenticated", {
|
|
||||||
provider: this.authProviderDisplayName(
|
|
||||||
this.authOptions?.auth_provider
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return EmberObject.create({
|
|
||||||
ok: true,
|
|
||||||
reason: i18n("user.email.ok"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
checkEmailAvailability() {
|
|
||||||
this.set("emailValidationVisible", Boolean(this.emailValidation.reason));
|
|
||||||
|
|
||||||
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("user.email.ok"),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.setProperties({
|
|
||||||
serverAccountEmail: null,
|
|
||||||
serverEmailValidation: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get emailDisabled() {
|
|
||||||
return (
|
|
||||||
this.authOptions?.email === this.accountEmail &&
|
|
||||||
this.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.accountUsername = "";
|
|
||||||
}
|
|
||||||
this.set("prefilledUsername", null);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this.get("emailValidation.ok") &&
|
|
||||||
(isEmpty(this.accountUsername) || this.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.usernameValidationHelper.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.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.field.id] = f.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set("formSubmitted", true);
|
|
||||||
return User.createAccount(attrs).then(
|
|
||||||
(result) => {
|
|
||||||
if (this.isDestroying || this.isDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.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("create_account.failed"));
|
|
||||||
if (result.is_developer) {
|
|
||||||
this.isDeveloper = true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
result.errors &&
|
|
||||||
result.errors.email &&
|
|
||||||
result.errors.email.length > 0 &&
|
|
||||||
result.values
|
|
||||||
) {
|
|
||||||
this.rejectedEmails.pushObject(result.values.email);
|
|
||||||
}
|
|
||||||
if (result.errors?.["user_password.password"]?.length > 0) {
|
|
||||||
this.passwordValidationHelper.rejectedPasswords.push(
|
|
||||||
attrs.accountPassword
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.set("formSubmitted", false);
|
|
||||||
removeCookie("destination_url");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.set("formSubmitted", false);
|
|
||||||
removeCookie("destination_url");
|
|
||||||
return this.set("flash", i18n("create_account.failed"));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get associateHtml() {
|
|
||||||
const url = this.authOptions?.associate_url;
|
|
||||||
if (!url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return i18n("create_account.associate", {
|
|
||||||
associate_link: url,
|
|
||||||
provider: i18n(`login.${this.authOptions.auth_provider}.name`),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
scrollInputIntoView(event) {
|
|
||||||
event.target.scrollIntoView({
|
|
||||||
behavior: "smooth",
|
|
||||||
block: "center",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@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.nameValidationHelper.forceValidationReason = true;
|
|
||||||
this.userFieldsValidationHelper.validationVisible = true;
|
|
||||||
this.set("emailValidationVisible", 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.userFieldsValidationHelper.validationVisible = false;
|
|
||||||
this.nameValidationHelper.forceValidationReason = false;
|
|
||||||
this.performAccountCreation();
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
{{! template-lint-disable no-duplicate-id }}
|
|
||||||
<DModal
|
|
||||||
class="create-account -large"
|
|
||||||
{{on "keydown" this.actionOnEnter}}
|
|
||||||
{{on "click" this.selectKitFocus}}
|
|
||||||
@closeModal={{@closeModal}}
|
|
||||||
@bodyClass={{this.modalBodyClasses}}
|
|
||||||
@flash={{this.flash}}
|
|
||||||
@flashType="error"
|
|
||||||
aria-labelledby="create-account-title"
|
|
||||||
>
|
|
||||||
<:body>
|
|
||||||
<PluginOutlet
|
|
||||||
@name="create-account-before-modal-body"
|
|
||||||
@connectorTagName="div"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class={{concatClass
|
|
||||||
(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"}}
|
|
||||||
>
|
|
||||||
<PluginOutlet
|
|
||||||
@name="create-account-header-bottom"
|
|
||||||
@outletArgs={{hash showLogin=(routeAction "showLogin")}}
|
|
||||||
/>
|
|
||||||
</WelcomeHeader>
|
|
||||||
{{#if this.showCreateForm}}
|
|
||||||
<form id="login-form">
|
|
||||||
{{#if this.associateHtml}}
|
|
||||||
<div class="input-group create-account-associate-link">
|
|
||||||
<span>{{htmlSafe this.associateHtml}}</span>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
<div class="input-group create-account-email">
|
|
||||||
<Input
|
|
||||||
{{on "focusout" this.checkEmailAvailability}}
|
|
||||||
{{on "focusin" this.scrollInputIntoView}}
|
|
||||||
@type="email"
|
|
||||||
@value={{this.accountEmail}}
|
|
||||||
disabled={{this.emailDisabled}}
|
|
||||||
autofocus="autofocus"
|
|
||||||
aria-describedby="account-email-validation account-email-validation-more-info"
|
|
||||||
aria-invalid={{this.emailValidation.failed}}
|
|
||||||
name="email"
|
|
||||||
id="new-account-email"
|
|
||||||
class={{valueEntered this.accountEmail}}
|
|
||||||
/>
|
|
||||||
<label class="alt-placeholder" for="new-account-email">
|
|
||||||
{{i18n "user.email.title"}}
|
|
||||||
</label>
|
|
||||||
{{#if this.showEmailValidation}}
|
|
||||||
<InputTip
|
|
||||||
@validation={{this.emailValidation}}
|
|
||||||
id="account-email-validation"
|
|
||||||
/>
|
|
||||||
{{else}}
|
|
||||||
<span
|
|
||||||
class="more-info"
|
|
||||||
id="account-email-validation-more-info"
|
|
||||||
>
|
|
||||||
{{#if
|
|
||||||
this.siteSettings.show_signup_form_email_instructions
|
|
||||||
}}
|
|
||||||
{{i18n "user.email.instructions"}}
|
|
||||||
{{/if}}
|
|
||||||
</span>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="input-group create-account__username">
|
|
||||||
<input
|
|
||||||
{{on "focusin" this.scrollInputIntoView}}
|
|
||||||
{{on "input" this.setAccountUsername}}
|
|
||||||
type="text"
|
|
||||||
value={{this.accountUsername}}
|
|
||||||
disabled={{this.usernameDisabled}}
|
|
||||||
maxlength={{this.maxUsernameLength}}
|
|
||||||
aria-describedby="username-validation username-validation-more-info"
|
|
||||||
aria-invalid={{this.usernameValidation.failed}}
|
|
||||||
autocomplete="off"
|
|
||||||
name="username"
|
|
||||||
id="new-account-username"
|
|
||||||
class={{valueEntered this.accountUsername}}
|
|
||||||
/>
|
|
||||||
<label class="alt-placeholder" for="new-account-username">
|
|
||||||
{{i18n "user.username.title"}}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{{#if this.showUsernameInstructions}}
|
|
||||||
<span class="more-info" id="username-validation-more-info">
|
|
||||||
{{i18n "user.username.instructions"}}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{{else}}
|
|
||||||
<InputTip
|
|
||||||
@validation={{this.usernameValidation}}
|
|
||||||
id="username-validation"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if (and this.showFullname this.fullnameRequired)}}
|
|
||||||
<FullnameInput
|
|
||||||
@nameValidation={{this.nameValidation}}
|
|
||||||
@nameTitle={{this.nameTitle}}
|
|
||||||
@accountName={{this.accountName}}
|
|
||||||
@nameDisabled={{this.nameDisabled}}
|
|
||||||
@onFocusIn={{this.scrollInputIntoView}}
|
|
||||||
class="input-group create-account__fullname required"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<PluginOutlet
|
|
||||||
@name="create-account-before-password"
|
|
||||||
@outletArgs={{hash
|
|
||||||
accountName=this.accountName
|
|
||||||
accountUsername=this.accountUsername
|
|
||||||
accountPassword=this.accountPassword
|
|
||||||
userFields=this.userFields
|
|
||||||
authOptions=this.authOptions
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="input-group create-account__password">
|
|
||||||
{{#if this.passwordRequired}}
|
|
||||||
<PasswordField
|
|
||||||
{{on "focusin" this.scrollInputIntoView}}
|
|
||||||
@value={{this.accountPassword}}
|
|
||||||
@capsLockOn={{this.capsLockOn}}
|
|
||||||
type={{if this.maskPassword "password" "text"}}
|
|
||||||
autocomplete="current-password"
|
|
||||||
aria-describedby="password-validation password-validation-more-info"
|
|
||||||
aria-invalid={{this.passwordValidation.failed}}
|
|
||||||
id="new-account-password"
|
|
||||||
class={{valueEntered this.accountPassword}}
|
|
||||||
/>
|
|
||||||
<label class="alt-placeholder" for="new-account-password">
|
|
||||||
{{i18n "user.password.title"}}
|
|
||||||
</label>
|
|
||||||
<TogglePasswordMask
|
|
||||||
@maskPassword={{this.maskPassword}}
|
|
||||||
@togglePasswordMask={{this.togglePasswordMask}}
|
|
||||||
/>
|
|
||||||
<div class="create-account__password-info">
|
|
||||||
<div class="create-account__password-tip-validation">
|
|
||||||
{{#if this.showPasswordValidation}}
|
|
||||||
<InputTip
|
|
||||||
@validation={{this.passwordValidation}}
|
|
||||||
id="password-validation"
|
|
||||||
/>
|
|
||||||
{{else if
|
|
||||||
this.siteSettings.show_signup_form_password_instructions
|
|
||||||
}}
|
|
||||||
<span
|
|
||||||
class="more-info"
|
|
||||||
id="password-validation-more-info"
|
|
||||||
>
|
|
||||||
{{this.passwordValidationHelper.passwordInstructions}}
|
|
||||||
</span>
|
|
||||||
{{/if}}
|
|
||||||
<div
|
|
||||||
class={{concatClass
|
|
||||||
"caps-lock-warning"
|
|
||||||
(unless this.capsLockOn "hidden")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{{icon "triangle-exclamation"}}
|
|
||||||
{{i18n "login.caps_lock_warning"}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<div class="password-confirmation">
|
|
||||||
<label for="new-account-password-confirmation">
|
|
||||||
{{i18n "user.password_confirmation.title"}}
|
|
||||||
</label>
|
|
||||||
<HoneypotInput
|
|
||||||
@id="new-account-confirmation"
|
|
||||||
@autocomplete="new-password"
|
|
||||||
@value={{this.accountHoneypot}}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
@value={{this.accountChallenge}}
|
|
||||||
id="new-account-challenge"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if this.requireInviteCode}}
|
|
||||||
<div class="input-group create-account__invite-code">
|
|
||||||
<Input
|
|
||||||
{{on "focusin" this.scrollInputIntoView}}
|
|
||||||
@value={{this.inviteCode}}
|
|
||||||
id="inviteCode"
|
|
||||||
class={{valueEntered 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
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{{#if (and this.showFullname (not this.fullnameRequired))}}
|
|
||||||
<FullnameInput
|
|
||||||
@nameValidation={{this.nameValidation}}
|
|
||||||
@nameTitle={{this.nameTitle}}
|
|
||||||
@accountName={{this.accountName}}
|
|
||||||
@nameDisabled={{this.nameDisabled}}
|
|
||||||
@onFocusIn={{this.scrollInputIntoView}}
|
|
||||||
class="input-group create-account__fullname"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.userFields}}
|
|
||||||
<div class="user-fields">
|
|
||||||
{{#each this.userFields as |f|}}
|
|
||||||
<div class="input-group">
|
|
||||||
<UserField
|
|
||||||
{{on "focusin" this.scrollInputIntoView}}
|
|
||||||
@field={{f.field}}
|
|
||||||
@value={{f.value}}
|
|
||||||
@validation={{f.validation}}
|
|
||||||
class={{valueEntered 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}}
|
|
||||||
<div class="d-modal__footer">
|
|
||||||
<SignupPageCta
|
|
||||||
@formSubmitted={{this.formSubmitted}}
|
|
||||||
@hasAuthOptions={{this.hasAuthOptions}}
|
|
||||||
@createAccount={{this.createAccount}}
|
|
||||||
@submitDisabled={{this.submitDisabled}}
|
|
||||||
@disclaimerHtml={{this.disclaimerHtml}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<PluginOutlet
|
|
||||||
@name="create-account-after-modal-footer"
|
|
||||||
@connectorTagName="div"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.skipConfirmation}}
|
|
||||||
{{loadingSpinner 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}}
|
|
||||||
</:body>
|
|
||||||
|
|
||||||
<:footer>
|
|
||||||
{{#if (and this.showCreateForm this.site.mobileView)}}
|
|
||||||
<SignupPageCta
|
|
||||||
@formSubmitted={{this.formSubmitted}}
|
|
||||||
@hasAuthOptions={{this.hasAuthOptions}}
|
|
||||||
@createAccount={{this.createAccount}}
|
|
||||||
@submitDisabled={{this.submitDisabled}}
|
|
||||||
@disclaimerHtml={{this.disclaimerHtml}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</:footer>
|
|
||||||
</DModal>
|
|
||||||
</template>
|
|
||||||
}
|
|
|
@ -1,526 +0,0 @@
|
||||||
import Component from "@glimmer/component";
|
|
||||||
import { tracked } from "@glimmer/tracking";
|
|
||||||
import { hash } from "@ember/helper";
|
|
||||||
import { on } from "@ember/modifier";
|
|
||||||
import { action } from "@ember/object";
|
|
||||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
|
||||||
import { schedule } from "@ember/runloop";
|
|
||||||
import { service } from "@ember/service";
|
|
||||||
import { htmlSafe } from "@ember/template";
|
|
||||||
import { isEmpty } from "@ember/utils";
|
|
||||||
import { and } from "truth-helpers";
|
|
||||||
import DModal from "discourse/components/d-modal";
|
|
||||||
import LocalLoginForm from "discourse/components/local-login-form";
|
|
||||||
import LoginButtons from "discourse/components/login-buttons";
|
|
||||||
import LoginPageCta from "discourse/components/login-page-cta";
|
|
||||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
|
||||||
import WelcomeHeader from "discourse/components/welcome-header";
|
|
||||||
import { ajax } from "discourse/lib/ajax";
|
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
||||||
import cookie, { removeCookie } from "discourse/lib/cookie";
|
|
||||||
import escape from "discourse/lib/escape";
|
|
||||||
import getURL from "discourse/lib/get-url";
|
|
||||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
|
||||||
import { areCookiesEnabled } from "discourse/lib/utilities";
|
|
||||||
import {
|
|
||||||
getPasskeyCredential,
|
|
||||||
isWebauthnSupported,
|
|
||||||
} from "discourse/lib/webauthn";
|
|
||||||
import { findAll } from "discourse/models/login-method";
|
|
||||||
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
import ForgotPassword from "./forgot-password";
|
|
||||||
|
|
||||||
export default class Login extends Component {
|
|
||||||
@service capabilities;
|
|
||||||
@service dialog;
|
|
||||||
@service siteSettings;
|
|
||||||
@service site;
|
|
||||||
@service login;
|
|
||||||
@service modal;
|
|
||||||
|
|
||||||
@tracked loggingIn = false;
|
|
||||||
@tracked loggedIn = false;
|
|
||||||
@tracked showLoginButtons = true;
|
|
||||||
@tracked showSecondFactor = false;
|
|
||||||
@tracked loginPassword = "";
|
|
||||||
@tracked loginName = "";
|
|
||||||
@tracked flash = this.args.model.flash;
|
|
||||||
@tracked flashType = this.args.model.flashType;
|
|
||||||
@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;
|
|
||||||
|
|
||||||
get awaitingApproval() {
|
|
||||||
return (
|
|
||||||
this.args.model.awaitingApproval &&
|
|
||||||
!this.canLoginLocal &&
|
|
||||||
!this.canLoginLocalWithEmail
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get loginDisabled() {
|
|
||||||
return this.loggingIn || this.loggedIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
get modalBodyClasses() {
|
|
||||||
const classes = ["login-modal-body"];
|
|
||||||
if (this.awaitingApproval) {
|
|
||||||
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.args.model.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) {
|
|
||||||
const authResult = await ajax("/session/passkey/auth.json", {
|
|
||||||
type: "POST",
|
|
||||||
data: { publicKeyCredential },
|
|
||||||
});
|
|
||||||
|
|
||||||
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 if (this.args.model.referrerTopicUrl) {
|
|
||||||
window.location.assign(this.args.model.referrerTopicUrl);
|
|
||||||
} 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
|
|
||||||
async triggerLogin() {
|
|
||||||
if (this.loginDisabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEmpty(this.loginName) || isEmpty(this.loginPassword)) {
|
|
||||||
this.flash = i18n("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;
|
|
||||||
|
|
||||||
// only need to focus the 2FA input for TOTP
|
|
||||||
if (!this.showSecurityKey) {
|
|
||||||
schedule("afterRender", () =>
|
|
||||||
document
|
|
||||||
.getElementById("second-factor")
|
|
||||||
.querySelector("input")
|
|
||||||
.focus()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
} else if (result.reason === "not_activated") {
|
|
||||||
this.args.model.showNotActivated({
|
|
||||||
username: this.loginName,
|
|
||||||
sentTo: escape(result.sent_to_email),
|
|
||||||
currentEmail: escape(result.current_email),
|
|
||||||
});
|
|
||||||
} else if (result.reason === "suspended") {
|
|
||||||
this.args.closeModal();
|
|
||||||
this.dialog.alert(result.error);
|
|
||||||
} else if (result.reason === "expired") {
|
|
||||||
this.flash = htmlSafe(
|
|
||||||
i18n("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 if (this.args.model.referrerTopicUrl) {
|
|
||||||
applyHiddenFormInputValue(
|
|
||||||
this.args.model.referrerTopicUrl,
|
|
||||||
"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("login.rate_limit");
|
|
||||||
this.flashType = "error";
|
|
||||||
} else if (
|
|
||||||
e.jqXHR &&
|
|
||||||
e.jqXHR.status === 503 &&
|
|
||||||
e.jqXHR.responseJSON.error_type === "read_only"
|
|
||||||
) {
|
|
||||||
this.flash = i18n("read_only_mode.login_disabled");
|
|
||||||
this.flashType = "error";
|
|
||||||
} else if (!areCookiesEnabled()) {
|
|
||||||
this.flash = i18n("login.cookies_error");
|
|
||||||
this.flashType = "error";
|
|
||||||
} else {
|
|
||||||
this.flash = i18n("login.error");
|
|
||||||
this.flashType = "error";
|
|
||||||
}
|
|
||||||
this.loggingIn = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
externalLoginAction(loginMethod) {
|
|
||||||
if (this.loginDisabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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.args.model.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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<DModal
|
|
||||||
class="login-modal -large"
|
|
||||||
@bodyClass={{this.modalBodyClasses}}
|
|
||||||
@closeModal={{@closeModal}}
|
|
||||||
@flash={{this.flash}}
|
|
||||||
@flashType={{this.flashType}}
|
|
||||||
{{didInsert this.preloadLogin}}
|
|
||||||
{{on "click" this.interceptResetLink}}
|
|
||||||
>
|
|
||||||
<:body>
|
|
||||||
<PluginOutlet @name="login-before-modal-body" @connectorTagName="div" />
|
|
||||||
|
|
||||||
{{#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">
|
|
||||||
{{htmlSafe
|
|
||||||
(i18n
|
|
||||||
"login.no_login_methods.description"
|
|
||||||
(hash adminLoginPath=this.adminLoginPath)
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
{{#if this.site.mobileView}}
|
|
||||||
<WelcomeHeader @header={{i18n "login.header_title"}}>
|
|
||||||
<PluginOutlet
|
|
||||||
@name="login-header-bottom"
|
|
||||||
@outletArgs={{hash createAccount=this.createAccount}}
|
|
||||||
/>
|
|
||||||
</WelcomeHeader>
|
|
||||||
{{#if this.showLoginButtons}}
|
|
||||||
<LoginButtons
|
|
||||||
@externalLogin={{this.externalLoginAction}}
|
|
||||||
@passkeyLogin={{this.passkeyLogin}}
|
|
||||||
@context="login"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.canLoginLocal}}
|
|
||||||
<div class={{if this.site.desktopView "login-left-side"}}>
|
|
||||||
{{#if this.site.desktopView}}
|
|
||||||
<WelcomeHeader @header={{i18n "login.header_title"}}>
|
|
||||||
<PluginOutlet
|
|
||||||
@name="login-header-bottom"
|
|
||||||
@outletArgs={{hash createAccount=this.createAccount}}
|
|
||||||
/>
|
|
||||||
</WelcomeHeader>
|
|
||||||
{{/if}}
|
|
||||||
<LocalLoginForm
|
|
||||||
@loginName={{this.loginName}}
|
|
||||||
@loginNameChanged={{this.loginNameChanged}}
|
|
||||||
@canLoginLocalWithEmail={{this.canLoginLocalWithEmail}}
|
|
||||||
@canUsePasskeys={{this.canUsePasskeys}}
|
|
||||||
@passkeyLogin={{this.passkeyLogin}}
|
|
||||||
@loginPassword={{this.loginPassword}}
|
|
||||||
@secondFactorMethod={{this.secondFactorMethod}}
|
|
||||||
@secondFactorToken={{this.secondFactorToken}}
|
|
||||||
@backupEnabled={{this.backupEnabled}}
|
|
||||||
@totpEnabled={{this.totpEnabled}}
|
|
||||||
@securityKeyAllowedCredentialIds={{this.securityKeyAllowedCredentialIds}}
|
|
||||||
@securityKeyChallenge={{this.securityKeyChallenge}}
|
|
||||||
@showSecurityKey={{this.showSecurityKey}}
|
|
||||||
@otherMethodAllowed={{this.otherMethodAllowed}}
|
|
||||||
@showSecondFactor={{this.showSecondFactor}}
|
|
||||||
@handleForgotPassword={{this.handleForgotPassword}}
|
|
||||||
@login={{this.triggerLogin}}
|
|
||||||
@flashChanged={{this.flashChanged}}
|
|
||||||
@flashTypeChanged={{this.flashTypeChanged}}
|
|
||||||
@securityKeyCredentialChanged={{this.securityKeyCredentialChanged}}
|
|
||||||
/>
|
|
||||||
{{#if this.site.desktopView}}
|
|
||||||
<div class="d-modal__footer">
|
|
||||||
<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}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if (and this.showLoginButtons this.site.desktopView)}}
|
|
||||||
{{#unless this.canLoginLocal}}
|
|
||||||
<div class="login-left-side">
|
|
||||||
<WelcomeHeader @header={{i18n "login.header_title"}} />
|
|
||||||
</div>
|
|
||||||
{{/unless}}
|
|
||||||
{{#if this.hasAtLeastOneLoginButton}}
|
|
||||||
<div class="login-right-side">
|
|
||||||
<LoginButtons
|
|
||||||
@externalLogin={{this.externalLoginAction}}
|
|
||||||
@passkeyLogin={{this.passkeyLogin}}
|
|
||||||
@context="login"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
</:body>
|
|
||||||
|
|
||||||
<:footer>
|
|
||||||
{{#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}}
|
|
||||||
</:footer>
|
|
||||||
</DModal>
|
|
||||||
</template>
|
|
||||||
}
|
|
|
@ -118,9 +118,7 @@ export default class LoginPageController extends Controller {
|
||||||
|
|
||||||
get shouldTriggerRouteAction() {
|
get shouldTriggerRouteAction() {
|
||||||
return (
|
return (
|
||||||
!this.siteSettings.full_page_login ||
|
this.siteSettings.enable_discourse_connect || this.singleExternalLogin
|
||||||
this.siteSettings.enable_discourse_connect ||
|
|
||||||
this.singleExternalLogin
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import { next } from "@ember/runloop";
|
import { next } from "@ember/runloop";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
import CreateAccount from "discourse/components/modal/create-account";
|
|
||||||
import LoginModal from "discourse/components/modal/login";
|
|
||||||
import cookie, { removeCookie } from "discourse/lib/cookie";
|
import cookie, { removeCookie } from "discourse/lib/cookie";
|
||||||
import DiscourseUrl from "discourse/lib/url";
|
import DiscourseUrl from "discourse/lib/url";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
|
@ -52,11 +50,9 @@ export default {
|
||||||
.lookup("controller:invites-show")
|
.lookup("controller:invites-show")
|
||||||
.authenticationComplete(options);
|
.authenticationComplete(options);
|
||||||
} else {
|
} else {
|
||||||
const modal = owner.lookup("service:modal");
|
|
||||||
const siteSettings = owner.lookup("service:site-settings");
|
const siteSettings = owner.lookup("service:site-settings");
|
||||||
|
|
||||||
const loginError = (errorMsg, className, properties, callback) => {
|
const loginError = (errorMsg, className, properties, callback) => {
|
||||||
const applicationRoute = owner.lookup("route:application");
|
|
||||||
const applicationController = owner.lookup(
|
const applicationController = owner.lookup(
|
||||||
"controller:application"
|
"controller:application"
|
||||||
);
|
);
|
||||||
|
@ -69,23 +65,12 @@ export default {
|
||||||
...properties,
|
...properties,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (siteSettings.full_page_login) {
|
router.transitionTo("login").then((login) => {
|
||||||
router.transitionTo("login").then((login) => {
|
Object.keys(loginProps || {}).forEach((key) => {
|
||||||
Object.keys(loginProps || {}).forEach((key) => {
|
login.controller.set(key, loginProps[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?.());
|
next(() => callback?.());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -139,16 +124,12 @@ export default {
|
||||||
skipConfirmation: siteSettings.auth_skip_create_confirm,
|
skipConfirmation: siteSettings.auth_skip_create_confirm,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (siteSettings.full_page_login) {
|
router.transitionTo("signup").then((signup) => {
|
||||||
router.transitionTo("signup").then((signup) => {
|
const signupController =
|
||||||
const signupController =
|
signup.controller || owner.lookup("controller:signup");
|
||||||
signup.controller || owner.lookup("controller:signup");
|
Object.assign(signupController, createAccountProps);
|
||||||
Object.assign(signupController, createAccountProps);
|
signupController.handleSkipConfirmation();
|
||||||
signupController.handleSkipConfirmation();
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
modal.show(CreateAccount, { model: createAccountProps });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import CreateAccount from "discourse/components/modal/create-account";
|
|
||||||
import KeyboardShortcutsHelp from "discourse/components/modal/keyboard-shortcuts-help";
|
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 NotActivatedModal from "discourse/components/modal/not-activated";
|
||||||
import { RouteException } from "discourse/controllers/exception";
|
import { RouteException } from "discourse/controllers/exception";
|
||||||
import { setting } from "discourse/lib/computed";
|
import { setting } from "discourse/lib/computed";
|
||||||
|
@ -270,24 +268,13 @@ export default class ApplicationRoute extends DiscourseRoute {
|
||||||
} else {
|
} else {
|
||||||
if (this.login.isOnlyOneExternalLoginMethod) {
|
if (this.login.isOnlyOneExternalLoginMethod) {
|
||||||
this.login.singleExternalLogin();
|
this.login.singleExternalLogin();
|
||||||
} else if (this.siteSettings.full_page_login) {
|
} else {
|
||||||
this.router.transitionTo("login").then((login) => {
|
this.router.transitionTo("login").then((login) => {
|
||||||
login.controller.set("canSignUp", this.controller.canSignUp);
|
login.controller.set("canSignUp", this.controller.canSignUp);
|
||||||
if (this.siteSettings.login_required) {
|
if (this.siteSettings.login_required) {
|
||||||
login.controller.set("showLogin", true);
|
login.controller.set("showLogin", true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
this.modal.show(LoginModal, {
|
|
||||||
model: {
|
|
||||||
showNotActivated: (props) => this.send("showNotActivated", props),
|
|
||||||
showCreateAccount: (props) => this.send("showCreateAccount", props),
|
|
||||||
canSignUp: this.controller.canSignUp,
|
|
||||||
referrerTopicUrl: DiscourseURL.isInternalTopic(document.referrer)
|
|
||||||
? document.referrer
|
|
||||||
: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -300,14 +287,12 @@ export default class ApplicationRoute extends DiscourseRoute {
|
||||||
if (this.login.isOnlyOneExternalLoginMethod) {
|
if (this.login.isOnlyOneExternalLoginMethod) {
|
||||||
// we will automatically redirect to the external auth service
|
// we will automatically redirect to the external auth service
|
||||||
this.login.singleExternalLogin({ signup: true });
|
this.login.singleExternalLogin({ signup: true });
|
||||||
} else if (this.siteSettings.full_page_login) {
|
} else {
|
||||||
this.router.transitionTo("signup").then((signup) => {
|
this.router.transitionTo("signup").then((signup) => {
|
||||||
Object.keys(createAccountProps || {}).forEach((key) => {
|
Object.keys(createAccountProps || {}).forEach((key) => {
|
||||||
signup.controller.set(key, createAccountProps[key]);
|
signup.controller.set(key, createAccountProps[key]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
this.modal.show(CreateAccount, { model: createAccountProps });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,15 +24,9 @@ export default class LoginRoute extends DiscourseRoute {
|
||||||
) {
|
) {
|
||||||
this.login.singleExternalLogin();
|
this.login.singleExternalLogin();
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (this.login.isOnlyOneExternalLoginMethod) {
|
||||||
this.login.isOnlyOneExternalLoginMethod &&
|
|
||||||
this.siteSettings.full_page_login
|
|
||||||
) {
|
|
||||||
this.login.singleExternalLogin();
|
this.login.singleExternalLogin();
|
||||||
} else if (
|
} else if (this.siteSettings.enable_discourse_connect) {
|
||||||
!this.siteSettings.full_page_login ||
|
|
||||||
this.siteSettings.enable_discourse_connect
|
|
||||||
) {
|
|
||||||
this.router
|
this.router
|
||||||
.replaceWith(`/${defaultHomepage()}`)
|
.replaceWith(`/${defaultHomepage()}`)
|
||||||
.followRedirects()
|
.followRedirects()
|
||||||
|
|
|
@ -33,16 +33,15 @@ export default class SignupRoute extends DiscourseRoute {
|
||||||
@action
|
@action
|
||||||
async showCreateAccount() {
|
async showCreateAccount() {
|
||||||
const { canSignUp } = this.controllerFor("application");
|
const { canSignUp } = this.controllerFor("application");
|
||||||
if (canSignUp && this.siteSettings.full_page_login) {
|
if (!canSignUp) {
|
||||||
return;
|
const route = await this.router
|
||||||
}
|
.replaceWith(
|
||||||
const route = await this.router
|
this.siteSettings.login_required ? "login" : "discovery.latest"
|
||||||
.replaceWith(
|
)
|
||||||
this.siteSettings.login_required ? "login" : "discovery.latest"
|
.followRedirects();
|
||||||
)
|
if (canSignUp) {
|
||||||
.followRedirects();
|
next(() => route.send("showCreateAccount"));
|
||||||
if (canSignUp) {
|
}
|
||||||
next(() => route.send("showCreateAccount"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,12 +29,7 @@ export default RouteTemplate(
|
||||||
{{loadingSpinner}}
|
{{loadingSpinner}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if
|
{{#if
|
||||||
(and
|
(or @controller.showLogin (not @controller.siteSettings.login_required))
|
||||||
@controller.siteSettings.full_page_login
|
|
||||||
(or
|
|
||||||
@controller.showLogin (not @controller.siteSettings.login_required)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
{{! Show the full page login form }}
|
{{! Show the full page login form }}
|
||||||
<div class="login-fullpage">
|
<div class="login-fullpage">
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { currentRouteName, visit } from "@ember/test-helpers";
|
|
||||||
import { test } from "qunit";
|
|
||||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
|
||||||
|
|
||||||
acceptance("Login redirect - anonymous", function (needs) {
|
|
||||||
needs.settings({ full_page_login: false });
|
|
||||||
|
|
||||||
test("redirects login to default homepage", async function (assert) {
|
|
||||||
await visit("/login");
|
|
||||||
assert.strictEqual(
|
|
||||||
currentRouteName(),
|
|
||||||
"discovery.latest",
|
|
||||||
"it works when latest is the homepage"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
acceptance("Login redirect - categories default", function (needs) {
|
|
||||||
needs.settings({
|
|
||||||
top_menu: "categories|latest|top|hot",
|
|
||||||
full_page_login: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
test("when site setting is categories", async function (assert) {
|
|
||||||
await visit("/login");
|
|
||||||
assert.strictEqual(
|
|
||||||
currentRouteName(),
|
|
||||||
"discovery.categories",
|
|
||||||
"it works when categories is the homepage"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -2,27 +2,8 @@ import { click, currentRouteName, visit } from "@ember/test-helpers";
|
||||||
import { test } from "qunit";
|
import { test } from "qunit";
|
||||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
|
||||||
acceptance("Login Required", function (needs) {
|
|
||||||
needs.settings({ login_required: true, full_page_login: false });
|
|
||||||
|
|
||||||
test("redirect", async function (assert) {
|
|
||||||
await visit("/latest");
|
|
||||||
assert.strictEqual(
|
|
||||||
currentRouteName(),
|
|
||||||
"login",
|
|
||||||
"it redirects them to login"
|
|
||||||
);
|
|
||||||
|
|
||||||
await click(".login-button");
|
|
||||||
assert.dom(".login-modal").exists("login modal is shown");
|
|
||||||
|
|
||||||
await click(".d-modal__header .modal-close");
|
|
||||||
assert.dom(".login-modal").doesNotExist("closes the login modal");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
acceptance("Login Required - Full page login", function (needs) {
|
acceptance("Login Required - Full page login", function (needs) {
|
||||||
needs.settings({ login_required: true, full_page_login: true });
|
needs.settings({ login_required: true });
|
||||||
|
|
||||||
test("page", async function (assert) {
|
test("page", async function (assert) {
|
||||||
await visit("/");
|
await visit("/");
|
||||||
|
|
|
@ -1,142 +0,0 @@
|
||||||
import { click, fillIn, tab, visit } from "@ember/test-helpers";
|
|
||||||
import { test } from "qunit";
|
|
||||||
import sinon from "sinon";
|
|
||||||
import { acceptance, chromeTest } from "discourse/tests/helpers/qunit-helpers";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
acceptance("Modal - Login", function (needs) {
|
|
||||||
needs.settings({
|
|
||||||
full_page_login: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
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 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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
acceptance("Modal - Login - With 2FA", function (needs) {
|
|
||||||
needs.settings({
|
|
||||||
enable_local_logins_via_email: true,
|
|
||||||
full_page_login: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
needs.pretender((server, helper) => {
|
|
||||||
server.post(`/session`, () =>
|
|
||||||
helper.response({
|
|
||||||
error: i18n("login.invalid_second_factor_code"),
|
|
||||||
multiple_second_factor_methods: false,
|
|
||||||
security_key_enabled: false,
|
|
||||||
totp_enabled: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("You can tab to 2FA login button", async function (assert) {
|
|
||||||
await visit("/");
|
|
||||||
await click("header .login-button");
|
|
||||||
|
|
||||||
await fillIn("#login-account-name", "isaac@discourse.org");
|
|
||||||
await fillIn("#login-account-password", "password");
|
|
||||||
await click("#login-button");
|
|
||||||
|
|
||||||
assert.dom("#login-second-factor").isFocused();
|
|
||||||
await tab();
|
|
||||||
assert.dom("#login-button").isFocused();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
acceptance("Login - With Passkeys enabled", function () {
|
|
||||||
test("Includes passkeys button and conditional UI", async function (assert) {
|
|
||||||
await visit("/");
|
|
||||||
await click("header .login-button");
|
|
||||||
|
|
||||||
assert.dom(".passkey-login-button").exists();
|
|
||||||
|
|
||||||
assert
|
|
||||||
.dom("#login-account-name")
|
|
||||||
.hasAttribute("autocomplete", "username webauthn");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
acceptance("Modal - Login - With Passkeys disabled", function (needs) {
|
|
||||||
needs.settings({
|
|
||||||
enable_passkeys: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Excludes passkeys button and conditional UI", async function (assert) {
|
|
||||||
await visit("/");
|
|
||||||
await click("header .login-button");
|
|
||||||
|
|
||||||
assert.dom(".passkey-login-button").doesNotExist();
|
|
||||||
assert.dom("#login-account-name").hasAttribute("autocomplete", "username");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
acceptance("Login - Passkeys on mobile", function (needs) {
|
|
||||||
needs.mobileView();
|
|
||||||
|
|
||||||
test("Includes passkeys button and conditional UI", async function (assert) {
|
|
||||||
await visit("/");
|
|
||||||
await click("header .login-button");
|
|
||||||
|
|
||||||
sinon.stub(navigator.credentials, "get").callsFake(function () {
|
|
||||||
return Promise.reject(new Error("credentials.get got called"));
|
|
||||||
});
|
|
||||||
|
|
||||||
assert
|
|
||||||
.dom("#login-account-name")
|
|
||||||
.hasAttribute("autocomplete", "username webauthn");
|
|
||||||
|
|
||||||
await click(".passkey-login-button");
|
|
||||||
|
|
||||||
// clicking the button triggers credentials.get
|
|
||||||
// but we can't really test that in frontend so an error is returned
|
|
||||||
assert.dom(".dialog-body").exists();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
acceptance("Login - With no way to login", function (needs) {
|
|
||||||
needs.settings({
|
|
||||||
enable_local_logins: false,
|
|
||||||
enable_facebook_logins: false,
|
|
||||||
});
|
|
||||||
needs.site({ auth_providers: [] });
|
|
||||||
|
|
||||||
test("Displays a helpful message", async function (assert) {
|
|
||||||
await visit("/");
|
|
||||||
await click("header .login-button");
|
|
||||||
|
|
||||||
assert.dom("#login-account-name").doesNotExist();
|
|
||||||
assert.dom("#login-button").doesNotExist();
|
|
||||||
assert.dom(".no-login-methods-configured").exists();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
acceptance("Login button", function () {
|
|
||||||
test("with custom event on webview", async function (assert) {
|
|
||||||
const capabilities = this.container.lookup("service:capabilities");
|
|
||||||
sinon.stub(capabilities, "isAppWebview").value(true);
|
|
||||||
|
|
||||||
window.ReactNativeWebView = {
|
|
||||||
postMessage: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const webviewSpy = sinon.spy(window.ReactNativeWebView, "postMessage");
|
|
||||||
|
|
||||||
await visit("/");
|
|
||||||
await click("header .login-button");
|
|
||||||
|
|
||||||
assert.true(
|
|
||||||
webviewSpy.withArgs('{"showLogin":true}').calledOnce,
|
|
||||||
"triggers postmessage event"
|
|
||||||
);
|
|
||||||
|
|
||||||
delete window.ReactNativeWebView;
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,42 +0,0 @@
|
||||||
import { click, fillIn, visit } from "@ember/test-helpers";
|
|
||||||
import { test } from "qunit";
|
|
||||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
const TOKEN = "sometoken";
|
|
||||||
|
|
||||||
acceptance("Login with email and 2FA", function (needs) {
|
|
||||||
needs.settings({
|
|
||||||
enable_local_logins_via_email: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
needs.pretender((server, helper) => {
|
|
||||||
server.post("/u/email-login", () =>
|
|
||||||
helper.response({
|
|
||||||
success: "OK",
|
|
||||||
user_found: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
server.get(`/session/email-login/${TOKEN}.json`, () =>
|
|
||||||
helper.response({
|
|
||||||
token: TOKEN,
|
|
||||||
can_login: true,
|
|
||||||
token_email: "blah@example.com",
|
|
||||||
security_key_required: true,
|
|
||||||
second_factor_required: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("You can switch from security key to 2FA", async function (assert) {
|
|
||||||
await visit("/");
|
|
||||||
await click("header .login-button");
|
|
||||||
await fillIn("#login-account-name", "blah@example.com");
|
|
||||||
await click("#email-login-link");
|
|
||||||
await visit(`/session/email-login/${TOKEN}`);
|
|
||||||
await click(".toggle-second-factor-method");
|
|
||||||
|
|
||||||
assert.dom("#second-factor").containsText(i18n("user.second_factor.title"));
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { click, fillIn, visit } from "@ember/test-helpers";
|
|
||||||
import { test } from "qunit";
|
|
||||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
acceptance("Login with email - hide email address taken", function (needs) {
|
|
||||||
needs.settings({
|
|
||||||
enable_local_logins_via_email: true,
|
|
||||||
hide_email_address_taken: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
needs.pretender((server, helper) => {
|
|
||||||
server.post("/u/email-login", () => {
|
|
||||||
return helper.response({ success: "OK" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("with hide_email_address_taken enabled", async function (assert) {
|
|
||||||
await visit("/");
|
|
||||||
await click("header .login-button");
|
|
||||||
await fillIn("#login-account-name", "someuser@example.com");
|
|
||||||
await click("#email-login-link");
|
|
||||||
|
|
||||||
assert.dom(".alert-success").hasHtml(
|
|
||||||
i18n("email_login.complete_email_found", {
|
|
||||||
email: "someuser@example.com",
|
|
||||||
}),
|
|
||||||
"displays the success message for any email address"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { click, visit } from "@ember/test-helpers";
|
|
||||||
import { test } from "qunit";
|
|
||||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
|
||||||
|
|
||||||
acceptance("Login with email - no social logins", function (needs) {
|
|
||||||
needs.settings({ enable_local_logins_via_email: true });
|
|
||||||
needs.pretender((server, helper) => {
|
|
||||||
server.post("/u/email-login", () => helper.response({ success: "OK" }));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("with login with email enabled", async function (assert) {
|
|
||||||
await visit("/");
|
|
||||||
await click("header .login-button");
|
|
||||||
|
|
||||||
assert.dom("#email-login-link").exists();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("with login with email disabled", async function (assert) {
|
|
||||||
await visit("/");
|
|
||||||
await click("header .login-button");
|
|
||||||
|
|
||||||
assert.dom(".login-buttons").doesNotExist();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { click, visit } from "@ember/test-helpers";
|
|
||||||
import { test } from "qunit";
|
|
||||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
|
||||||
|
|
||||||
acceptance("Login with email disabled", function (needs) {
|
|
||||||
needs.settings({
|
|
||||||
enable_local_logins_via_email: false,
|
|
||||||
enable_facebook_logins: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
test("with email button", async function (assert) {
|
|
||||||
await visit("/");
|
|
||||||
await click("header .login-button");
|
|
||||||
|
|
||||||
assert
|
|
||||||
.dom(".btn-social.facebook")
|
|
||||||
.exists("it displays the facebook login button");
|
|
||||||
|
|
||||||
assert
|
|
||||||
.dom("#email-login-link")
|
|
||||||
.doesNotExist("it displays the login with email button");
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,113 +0,0 @@
|
||||||
import { click, fillIn, visit } from "@ember/test-helpers";
|
|
||||||
import { test } from "qunit";
|
|
||||||
import sinon from "sinon";
|
|
||||||
import DiscourseURL from "discourse/lib/url";
|
|
||||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
const TOKEN = "sometoken";
|
|
||||||
|
|
||||||
acceptance("Login with email", function (needs) {
|
|
||||||
needs.settings({
|
|
||||||
enable_local_logins_via_email: true,
|
|
||||||
enable_facebook_logins: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let userFound = false;
|
|
||||||
needs.pretender((server, helper) => {
|
|
||||||
server.post("/u/email-login", () =>
|
|
||||||
helper.response({ success: "OK", user_found: userFound })
|
|
||||||
);
|
|
||||||
|
|
||||||
server.get(`/session/email-login/${TOKEN}.json`, () =>
|
|
||||||
helper.response({
|
|
||||||
token: TOKEN,
|
|
||||||
can_login: true,
|
|
||||||
token_email: "blah@example.com",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
server.post(`/session/email-login/${TOKEN}`, () =>
|
|
||||||
helper.response({
|
|
||||||
success: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("with email button", async function (assert) {
|
|
||||||
await visit("/");
|
|
||||||
await click("header .login-button");
|
|
||||||
|
|
||||||
assert
|
|
||||||
.dom(".btn-social.facebook")
|
|
||||||
.exists("it displays the facebook login button");
|
|
||||||
|
|
||||||
assert
|
|
||||||
.dom("#email-login-link")
|
|
||||||
.exists("it displays the login with email button");
|
|
||||||
|
|
||||||
await fillIn("#login-account-name", "someuser");
|
|
||||||
await click("#email-login-link");
|
|
||||||
|
|
||||||
assert.dom(".alert-error").hasHtml(
|
|
||||||
i18n("email_login.complete_username_not_found", {
|
|
||||||
username: "someuser",
|
|
||||||
}),
|
|
||||||
"displays an error for an invalid username"
|
|
||||||
);
|
|
||||||
|
|
||||||
await fillIn("#login-account-name", "someuser@gmail.com");
|
|
||||||
await click("#email-login-link");
|
|
||||||
|
|
||||||
assert.dom(".alert-error").hasHtml(
|
|
||||||
i18n("email_login.complete_email_not_found", {
|
|
||||||
email: "someuser@gmail.com",
|
|
||||||
}),
|
|
||||||
"displays an error for an invalid email"
|
|
||||||
);
|
|
||||||
|
|
||||||
await fillIn("#login-account-name", "someuser");
|
|
||||||
|
|
||||||
userFound = true;
|
|
||||||
|
|
||||||
await click("#email-login-link");
|
|
||||||
|
|
||||||
assert
|
|
||||||
.dom(".alert-success")
|
|
||||||
.hasHtml(
|
|
||||||
i18n("email_login.complete_username_found", { username: "someuser" }),
|
|
||||||
"displays a success message for a valid username"
|
|
||||||
);
|
|
||||||
|
|
||||||
await visit("/");
|
|
||||||
await click("header .login-button");
|
|
||||||
await fillIn("#login-account-name", "someuser@gmail.com");
|
|
||||||
await click("#email-login-link");
|
|
||||||
|
|
||||||
assert.dom(".alert-success").hasHtml(
|
|
||||||
i18n("email_login.complete_email_found", {
|
|
||||||
email: "someuser@gmail.com",
|
|
||||||
}),
|
|
||||||
"displays a success message for a valid email"
|
|
||||||
);
|
|
||||||
|
|
||||||
userFound = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
test("finish login UI", async function (assert) {
|
|
||||||
await visit(`/session/email-login/${TOKEN}`);
|
|
||||||
sinon.stub(DiscourseURL, "redirectTo");
|
|
||||||
await click(".email-login .btn-primary");
|
|
||||||
assert.true(DiscourseURL.redirectTo.calledWith("/"), "redirects to home");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("finish login UI - safe mode", async function (assert) {
|
|
||||||
await visit(`/session/email-login/${TOKEN}?safe_mode=no_themes,no_plugins`);
|
|
||||||
sinon.stub(DiscourseURL, "redirectTo");
|
|
||||||
await click(".email-login .btn-primary");
|
|
||||||
assert.true(
|
|
||||||
DiscourseURL.redirectTo.calledWith("/?safe_mode=no_themes%2Cno_plugins"),
|
|
||||||
"redirects to home with safe mode"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -41,17 +41,6 @@ acceptance("Static pages", function () {
|
||||||
assert.dom(".body-page").exists("The content is present");
|
assert.dom(".body-page").exists("The content is present");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Login redirect", async function (assert) {
|
|
||||||
this.siteSettings.full_page_login = false;
|
|
||||||
await visit("/login");
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
currentRouteName(),
|
|
||||||
"discovery.latest",
|
|
||||||
"it redirects to /latest"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Login-required page", async function (assert) {
|
test("Login-required page", async function (assert) {
|
||||||
this.siteSettings.login_required = true;
|
this.siteSettings.login_required = true;
|
||||||
await visit("/login");
|
await visit("/login");
|
||||||
|
@ -61,24 +50,4 @@ acceptance("Static pages", function () {
|
||||||
assert.dom(".sign-up-button").exists();
|
assert.dom(".sign-up-button").exists();
|
||||||
assert.dom(".login-button").exists();
|
assert.dom(".login-button").exists();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Signup redirect", async function (assert) {
|
|
||||||
this.siteSettings.full_page_login = false;
|
|
||||||
await visit("/signup");
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
currentRouteName(),
|
|
||||||
"discovery.latest",
|
|
||||||
"it redirects to /latest"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Signup redirect with login_required", async function (assert) {
|
|
||||||
this.siteSettings.full_page_login = false;
|
|
||||||
this.siteSettings.login_required = true;
|
|
||||||
await visit("/signup");
|
|
||||||
|
|
||||||
assert.strictEqual(currentRouteName(), "login");
|
|
||||||
assert.dom(".body-page").exists("The content is present");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
import { getOwner } from "@ember/owner";
|
|
||||||
import { settled } from "@ember/test-helpers";
|
|
||||||
import { setupTest } from "ember-qunit";
|
|
||||||
import { module, test } from "qunit";
|
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
module("Unit | Component | create-account", function (hooks) {
|
|
||||||
setupTest(hooks);
|
|
||||||
|
|
||||||
test("basicUsernameValidation", function (assert) {
|
|
||||||
const testInvalidUsername = (username, expectedReason) => {
|
|
||||||
const component = this.owner
|
|
||||||
.factoryFor("component:modal/create-account")
|
|
||||||
.create({ model: { accountUsername: username } });
|
|
||||||
|
|
||||||
const validation =
|
|
||||||
component.usernameValidationHelper.basicUsernameValidation(username);
|
|
||||||
assert.true(validation.failed, `username should be invalid: ${username}`);
|
|
||||||
assert.strictEqual(
|
|
||||||
validation.reason,
|
|
||||||
expectedReason,
|
|
||||||
`username validation reason: ${username}, ${expectedReason}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
testInvalidUsername("", null);
|
|
||||||
testInvalidUsername("x", i18n("user.username.too_short"));
|
|
||||||
testInvalidUsername(
|
|
||||||
"123456789012345678901",
|
|
||||||
i18n("user.username.too_long")
|
|
||||||
);
|
|
||||||
|
|
||||||
const component = this.owner
|
|
||||||
.factoryFor("component:modal/create-account")
|
|
||||||
.create({ model: { accountUsername: "porkchops" } });
|
|
||||||
component.set("prefilledUsername", "porkchops");
|
|
||||||
|
|
||||||
const validation =
|
|
||||||
component.usernameValidationHelper.basicUsernameValidation("porkchops");
|
|
||||||
assert.true(validation.ok, "Prefilled username is valid");
|
|
||||||
assert.strictEqual(
|
|
||||||
validation.reason,
|
|
||||||
i18n("user.username.prefilled"),
|
|
||||||
"Prefilled username is valid"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("passwordValidation", async function (assert) {
|
|
||||||
const component = this.owner
|
|
||||||
.factoryFor("component:modal/create-account")
|
|
||||||
.create({
|
|
||||||
model: {
|
|
||||||
accountEmail: "pork@chops.com",
|
|
||||||
accountUsername: "porkchops123",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
component.set("prefilledUsername", "porkchops123");
|
|
||||||
component.set("accountPassword", "b4fcdae11f9167");
|
|
||||||
assert.true(component.passwordValidation.ok, "Password is ok");
|
|
||||||
assert.strictEqual(
|
|
||||||
component.passwordValidation.reason,
|
|
||||||
i18n("user.password.ok"),
|
|
||||||
"Password is valid"
|
|
||||||
);
|
|
||||||
|
|
||||||
const testInvalidPassword = (password, expectedReason) => {
|
|
||||||
component.set("accountPassword", password);
|
|
||||||
|
|
||||||
assert.true(
|
|
||||||
component.passwordValidation.failed,
|
|
||||||
`password should be invalid: ${password}`
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
component.passwordValidation.reason,
|
|
||||||
expectedReason,
|
|
||||||
`password validation reason: ${password}, ${expectedReason}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const siteSettings = getOwner(this).lookup("service:site-settings");
|
|
||||||
testInvalidPassword("", null);
|
|
||||||
testInvalidPassword(
|
|
||||||
"x",
|
|
||||||
i18n("user.password.too_short", {
|
|
||||||
count: siteSettings.min_password_length,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
testInvalidPassword("porkchops123", i18n("user.password.same_as_username"));
|
|
||||||
testInvalidPassword("pork@chops.com", i18n("user.password.same_as_email"));
|
|
||||||
|
|
||||||
// Wait for username check request to finish
|
|
||||||
await settled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("authProviderDisplayName", function (assert) {
|
|
||||||
const component = this.owner
|
|
||||||
.factoryFor("component:modal/create-account")
|
|
||||||
.create({ model: {} });
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
component.authProviderDisplayName("facebook"),
|
|
||||||
i18n("login.facebook.name"),
|
|
||||||
"provider name is translated correctly"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
component.authProviderDisplayName("does-not-exist"),
|
|
||||||
"does-not-exist",
|
|
||||||
"provider name falls back if not found"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,3 +1,2 @@
|
||||||
@import "login-modal";
|
|
||||||
@import "modal-overrides";
|
@import "modal-overrides";
|
||||||
@import "user-status";
|
@import "user-status";
|
||||||
|
|
|
@ -1,446 +0,0 @@
|
||||||
.d-modal.login-modal,
|
|
||||||
.d-modal.create-account {
|
|
||||||
&:not(:has(.login-right-side)) .d-modal__container {
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.awaiting-approval {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-modal {
|
|
||||||
&__container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__header {
|
|
||||||
border-bottom: none;
|
|
||||||
padding: 0;
|
|
||||||
position: absolute;
|
|
||||||
top: 0.75em;
|
|
||||||
right: 0.75em;
|
|
||||||
z-index: z("max");
|
|
||||||
}
|
|
||||||
|
|
||||||
&__body {
|
|
||||||
display: flex;
|
|
||||||
gap: 2rem;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__footer {
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
|
|
||||||
.inline-spinner {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-left-side {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
padding: 3rem;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-right-side {
|
|
||||||
background: var(--tertiary-or-tertiary-low);
|
|
||||||
padding: 3.5rem 3rem;
|
|
||||||
max-width: 16em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-social-title {
|
|
||||||
@include ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 1rem;
|
|
||||||
height: 100%;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login-form {
|
|
||||||
margin: 2em 0 1em 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.create-account-associate-link {
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tip {
|
|
||||||
font-size: var(--font-down-1);
|
|
||||||
min-height: 1.4em;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
&.bad {
|
|
||||||
color: var(--danger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-password-mask span {
|
|
||||||
font-size: var(--font-down-1-rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.more-info,
|
|
||||||
.instructions {
|
|
||||||
font-size: var(--font-down-1);
|
|
||||||
color: var(--primary-medium);
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.caps-lock-warning {
|
|
||||||
color: var(--danger);
|
|
||||||
font-size: var(--font-down-1);
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#modal-alert {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
// if you want to use flexbox here make sure child elements like <b> don't cause line breaks
|
|
||||||
padding: 1em 3.5em 1em 1em; // large right padding to make sure long text isn't under the close button
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
margin-bottom: 0;
|
|
||||||
min-height: 35px;
|
|
||||||
|
|
||||||
&:empty {
|
|
||||||
min-height: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-page-cta,
|
|
||||||
.signup-page-cta {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&__disclaimer {
|
|
||||||
color: var(--primary-medium);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__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 {
|
|
||||||
color: var(--primary-medium);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktop-view & {
|
|
||||||
@media screen and (width <= 767px) {
|
|
||||||
// important to maintain narrow desktop widths
|
|
||||||
// for auth modals in Discourse Hub on iPad
|
|
||||||
.d-modal__header {
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.has-alt-auth {
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: auto;
|
|
||||||
gap: 0;
|
|
||||||
|
|
||||||
.d-modal__footer,
|
|
||||||
.btn-social {
|
|
||||||
font-size: var(--font-down-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-left-side {
|
|
||||||
overflow: unset;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-right-side {
|
|
||||||
padding: 1em;
|
|
||||||
max-width: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login-form {
|
|
||||||
margin: 1.5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-progress-bar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.invites-show {
|
|
||||||
#account-email-validation:not(:has(svg)) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-modal.create-account {
|
|
||||||
.d-modal {
|
|
||||||
&__container {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__footer {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__footer-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.already-have-account {
|
|
||||||
color: var(--primary-medium);
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
button,
|
|
||||||
hr {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.disclaimer {
|
|
||||||
color: var(--primary-medium);
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-account__password-info {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.create-account__password-tip-validation {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login Form Styles
|
|
||||||
.login-modal,
|
|
||||||
.create-account,
|
|
||||||
.invites-show {
|
|
||||||
#login-form,
|
|
||||||
.login-form,
|
|
||||||
.invite-form {
|
|
||||||
.input-group {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
|
|
||||||
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: 13px;
|
|
||||||
box-shadow: 0 0 0 0 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 label.control-label {
|
|
||||||
top: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-field.dropdown,
|
|
||||||
.user-field.multiselect {
|
|
||||||
.more-info,
|
|
||||||
.instructions {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// user fields input groups will
|
|
||||||
// be styled differently
|
|
||||||
.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;
|
|
||||||
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 {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
input[type="checkbox"].ember-checkbox {
|
|
||||||
width: 1em !important;
|
|
||||||
min-width: unset;
|
|
||||||
margin-block: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
@import "discourse";
|
@import "discourse";
|
||||||
@import "invite-signup";
|
@import "invite-signup";
|
||||||
@import "list-controls";
|
@import "list-controls";
|
||||||
@import "login-modal";
|
|
||||||
@import "login-signup-page";
|
@import "login-signup-page";
|
||||||
@import "menu-panel";
|
@import "menu-panel";
|
||||||
@import "modal";
|
@import "modal";
|
||||||
|
|
|
@ -1,292 +0,0 @@
|
||||||
.static-login {
|
|
||||||
#main-outlet-wrapper,
|
|
||||||
#main-outlet {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contents.body-page {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-welcome {
|
|
||||||
gap: 2em;
|
|
||||||
height: 100svh;
|
|
||||||
width: 100svw;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
.body-page-button-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1em;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-modal.login-modal .d-modal__body {
|
|
||||||
margin-top: 3.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-modal.login-modal,
|
|
||||||
.d-modal.create-account {
|
|
||||||
padding-bottom: var(--safe-area-inset-bottom);
|
|
||||||
|
|
||||||
.d-modal {
|
|
||||||
&__container {
|
|
||||||
height: 100svh;
|
|
||||||
max-height: unset;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__body {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: unset;
|
|
||||||
padding-inline: 0.5rem;
|
|
||||||
padding-bottom: 1em;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__footer {
|
|
||||||
padding: 1em 1.5rem;
|
|
||||||
border-top: 1px solid var(--primary-low);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__footer-buttons {
|
|
||||||
gap: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-title {
|
|
||||||
font-size: var(--font-up-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
font-size: var(--font-up-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: 0.75rem;
|
|
||||||
|
|
||||||
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-buttons {
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 1rem;
|
|
||||||
gap: 0.25em;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
height: unset;
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
margin: 0;
|
|
||||||
padding-block: 0.65rem;
|
|
||||||
border: 1px solid var(--primary-low);
|
|
||||||
flex-grow: 1;
|
|
||||||
font-size: var(--font-down-1);
|
|
||||||
white-space: nowrap;
|
|
||||||
min-width: calc(50% - 0.25em);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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-cta {
|
|
||||||
&__buttons {
|
|
||||||
.signup-page-cta__signup {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__login {
|
|
||||||
background: none !important;
|
|
||||||
font-size: var(--font-down);
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-page-cta,
|
|
||||||
.signup-page-cta {
|
|
||||||
&__existing-account,
|
|
||||||
&__no-account-yet {
|
|
||||||
width: unset;
|
|
||||||
margin-bottom: 0;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__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 1rem 0;
|
|
||||||
|
|
||||||
.input-group {
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-bottom: 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.more-info,
|
|
||||||
.tip,
|
|
||||||
.instructions {
|
|
||||||
font-size: var(--font-down-1-rem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-fields {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.caps-lock-warning {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-modal,
|
|
||||||
.create-account,
|
|
||||||
.invites-show {
|
|
||||||
#login-form,
|
|
||||||
.login-form,
|
|
||||||
.invite-form {
|
|
||||||
.input-group {
|
|
||||||
label.alt-placeholder,
|
|
||||||
.user-field.text label.control-label {
|
|
||||||
top: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.user-field:not(.dropdown)
|
|
||||||
.controls
|
|
||||||
input:focus
|
|
||||||
+ label.alt-placeholder.control-label,
|
|
||||||
div.user-field.value-entered:not(.dropdown)
|
|
||||||
.controls
|
|
||||||
label.alt-placeholder.control-label {
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-field.dropdown label.control-label,
|
|
||||||
.user-field.multiselect label.control-label {
|
|
||||||
top: -10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-created,
|
|
||||||
.activate-account {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-sizing: border-box;
|
|
||||||
max-width: unset;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.75em 1rem;
|
|
||||||
box-shadow: none;
|
|
||||||
|
|
||||||
.activate-new-email {
|
|
||||||
width: 100%;
|
|
||||||
height: 2.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activation-controls {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1em;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: 0.75em;
|
|
||||||
|
|
||||||
button {
|
|
||||||
height: 2.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.activate-account-button {
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: 0.75em;
|
|
||||||
width: 100%;
|
|
||||||
height: 2.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-activated {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-grow: 1;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.tada-image {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.continue-button {
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: 0.75em;
|
|
||||||
width: 100%;
|
|
||||||
height: 2.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -66,7 +66,6 @@ class ProblemCheck
|
||||||
ProblemCheck::FacebookConfig,
|
ProblemCheck::FacebookConfig,
|
||||||
ProblemCheck::FailingEmails,
|
ProblemCheck::FailingEmails,
|
||||||
ProblemCheck::ForceHttps,
|
ProblemCheck::ForceHttps,
|
||||||
ProblemCheck::FullPageLoginCheck,
|
|
||||||
ProblemCheck::GithubConfig,
|
ProblemCheck::GithubConfig,
|
||||||
ProblemCheck::GoogleAnalyticsVersion,
|
ProblemCheck::GoogleAnalyticsVersion,
|
||||||
ProblemCheck::GoogleOauth2Config,
|
ProblemCheck::GoogleOauth2Config,
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class ProblemCheck::FullPageLoginCheck < ProblemCheck
|
|
||||||
self.priority = "low"
|
|
||||||
|
|
||||||
def call
|
|
||||||
if full_page_login_disabled?
|
|
||||||
return problem(override_key: "dashboard.problem.full_page_login_check")
|
|
||||||
end
|
|
||||||
|
|
||||||
no_problem
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def full_page_login_disabled?
|
|
||||||
SiteSetting.full_page_login == false
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1736,7 +1736,6 @@ en:
|
||||||
category_style_deprecated: "Your Discourse is currently using a deprecated category style which will be removed before the final beta release of Discourse 3.2. Please refer to <a href='https://meta.discourse.org/t/282441'>Moving to a Single Category Style Site Setting</a> for instructions on how to keep your selected category style."
|
category_style_deprecated: "Your Discourse is currently using a deprecated category style which will be removed before the final beta release of Discourse 3.2. Please refer to <a href='https://meta.discourse.org/t/282441'>Moving to a Single Category Style Site Setting</a> for instructions on how to keep your selected category style."
|
||||||
maxmind_db_configuration: 'The server has been configured to use MaxMind databases for reverse IP lookups but a valid MaxMind account ID has not been configured which may result in MaxMind databases failing to download in the future. <a href="https://meta.discourse.org/t/configure-maxmind-for-reverse-ip-lookups/173941" target="_blank">See this guide to learn more</a>.'
|
maxmind_db_configuration: 'The server has been configured to use MaxMind databases for reverse IP lookups but a valid MaxMind account ID has not been configured which may result in MaxMind databases failing to download in the future. <a href="https://meta.discourse.org/t/configure-maxmind-for-reverse-ip-lookups/173941" target="_blank">See this guide to learn more</a>.'
|
||||||
admin_sidebar_deprecation: "The old admin layout is deprecated in favour of the new <a href='https://meta.discourse.org/t/-/289281'>sidebar layout</a> and will be removed in the next release. You can <a href='%{base_path}/admin/config/navigation?filter=admin%20sidebar'>configure</a> the new sidebar layout now to opt in before that."
|
admin_sidebar_deprecation: "The old admin layout is deprecated in favour of the new <a href='https://meta.discourse.org/t/-/289281'>sidebar layout</a> and will be removed in the next release. You can <a href='%{base_path}/admin/config/navigation?filter=admin%20sidebar'>configure</a> the new sidebar layout now to opt in before that."
|
||||||
full_page_login_check: "Your site has disabled the full page login setting. This setting is deprecated and will be removed on <strong>29 April 2025</strong>. Your site will be updated to use full page signup and login screens on that date. <a href='https://meta.discourse.org/t/introducing-our-new-fullscreen-signup-and-login-pages/340401'>Learn more</a>."
|
|
||||||
back_from_logster_text: "Back to site"
|
back_from_logster_text: "Back to site"
|
||||||
|
|
||||||
site_settings:
|
site_settings:
|
||||||
|
@ -1905,7 +1904,6 @@ 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."
|
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"
|
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"
|
maximum_session_age: "User will remain logged in for n hours since last visit"
|
||||||
full_page_login: "Show the login and signup forms in a full page (when unchecked, users will see the forms in a modal). "
|
|
||||||
show_signup_form_email_instructions: Show email instructions on the signup form.
|
show_signup_form_email_instructions: Show email instructions on the signup form.
|
||||||
show_signup_form_username_instructions: Show username instructions on the signup form.
|
show_signup_form_username_instructions: Show username instructions on the signup form.
|
||||||
show_signup_form_full_name_instructions: Show full name instructions on the signup form.
|
show_signup_form_full_name_instructions: Show full name instructions on the signup form.
|
||||||
|
|
|
@ -723,9 +723,6 @@ login:
|
||||||
default: 1440
|
default: 1440
|
||||||
min: 1
|
min: 1
|
||||||
max: 175200
|
max: 175200
|
||||||
full_page_login:
|
|
||||||
default: true
|
|
||||||
client: true
|
|
||||||
show_signup_form_email_instructions:
|
show_signup_form_email_instructions:
|
||||||
client: true
|
client: true
|
||||||
default: true
|
default: true
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
class RemoveFullPageLoginProblemCheckTrackers < ActiveRecord::Migration[7.2]
|
||||||
|
def up
|
||||||
|
execute(<<~SQL)
|
||||||
|
DELETE FROM problem_check_trackers WHERE identifier = 'full_page_login_check';
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise ActiveRecord::IrreversibleMigration
|
||||||
|
end
|
||||||
|
end
|
10
db/migrate/20250407042814_delete_full_page_login_setting.rb
Normal file
10
db/migrate/20250407042814_delete_full_page_login_setting.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
class DeleteFullPageLoginSetting < ActiveRecord::Migration[7.2]
|
||||||
|
def up
|
||||||
|
execute "DELETE FROM site_settings WHERE name = 'full_page_login'"
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise ActiveRecord::IrreversibleMigration
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,19 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
RSpec.describe ProblemCheck::FullPageLoginCheck do
|
|
||||||
let(:check) { described_class.new }
|
|
||||||
|
|
||||||
describe "#call" do
|
|
||||||
context "when full_page_login is enabled" do
|
|
||||||
before { SiteSetting.full_page_login = true }
|
|
||||||
|
|
||||||
it { expect(check).to be_chill_about_it }
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when full_page_login is enabled" do
|
|
||||||
before { SiteSetting.full_page_login = false }
|
|
||||||
|
|
||||||
it { expect(check).to have_a_problem.with_priority("low") }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -10,16 +10,16 @@ RSpec.shared_examples_for "having working core features" do |skip_examples: []|
|
||||||
|
|
||||||
if skip_examples.exclude?(:login)
|
if skip_examples.exclude?(:login)
|
||||||
describe "Login" do
|
describe "Login" do
|
||||||
let(:login_form) { PageObjects::Modals::Login.new }
|
let(:login_form) { PageObjects::Pages::Login.new }
|
||||||
|
|
||||||
before { EmailToken.confirm(Fabricate(:email_token, user: active_user).token) }
|
before { EmailToken.confirm(Fabricate(:email_token, user: active_user).token) }
|
||||||
|
|
||||||
it "logs in" do
|
it "logs in" do
|
||||||
visit("/")
|
visit("/")
|
||||||
login_form
|
login_form.open
|
||||||
.open
|
login_form.fill_username(active_user.username)
|
||||||
.fill(username: active_user.username, password: "secure_password")
|
login_form.fill_password("secure_password")
|
||||||
.click_login
|
login_form.click_login
|
||||||
expect(page).to have_css(".current-user", visible: true)
|
expect(page).to have_css(".current-user", visible: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,6 @@ describe "Changing email", type: :system do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not require login to confirm email change" do
|
it "does not require login to confirm email change" do
|
||||||
SiteSetting.full_page_login = false
|
|
||||||
second_factor = Fabricate(:user_second_factor_totp, user: user)
|
second_factor = Fabricate(:user_second_factor_totp, user: user)
|
||||||
sign_in user
|
sign_in user
|
||||||
|
|
||||||
|
|
|
@ -360,12 +360,10 @@ end
|
||||||
|
|
||||||
describe "Login", type: :system do
|
describe "Login", type: :system do
|
||||||
context "when fullpage desktop" do
|
context "when fullpage desktop" do
|
||||||
before { SiteSetting.full_page_login = true }
|
|
||||||
include_examples "login scenarios", PageObjects::Pages::Login.new
|
include_examples "login scenarios", PageObjects::Pages::Login.new
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when fullpage mobile", mobile: true do
|
context "when fullpage mobile", mobile: true do
|
||||||
before { SiteSetting.full_page_login = true }
|
|
||||||
include_examples "login scenarios", PageObjects::Pages::Login.new
|
include_examples "login scenarios", PageObjects::Pages::Login.new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module PageObjects
|
|
||||||
module Modals
|
|
||||||
class Login < PageObjects::Modals::Base
|
|
||||||
def open?
|
|
||||||
super && has_css?(".login-modal")
|
|
||||||
end
|
|
||||||
|
|
||||||
def closed?
|
|
||||||
super && has_no_css?(".login-modal")
|
|
||||||
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_css(".d-modal:not(.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_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)
|
|
||||||
click(".btn-social.#{provider}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,126 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module PageObjects
|
|
||||||
module Modals
|
|
||||||
class Signup < PageObjects::Modals::Base
|
|
||||||
def open?
|
|
||||||
super && has_css?(".modal.create-account")
|
|
||||||
end
|
|
||||||
|
|
||||||
def closed?
|
|
||||||
super && has_no_css?(".modal.create-account")
|
|
||||||
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_css(".d-modal:not(.is-animating)")
|
|
||||||
end
|
|
||||||
find(selector).click
|
|
||||||
end
|
|
||||||
|
|
||||||
def open_login
|
|
||||||
click("#login-link")
|
|
||||||
end
|
|
||||||
|
|
||||||
def click_create_account
|
|
||||||
click(".modal.create-account .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 has_name_input?
|
|
||||||
has_css?("#new-account-name")
|
|
||||||
end
|
|
||||||
|
|
||||||
def has_no_name_input?
|
|
||||||
has_no_css?("#new-account-name")
|
|
||||||
end
|
|
||||||
|
|
||||||
def fill_input(selector, text)
|
|
||||||
if page.has_css?("html.mobile-view", wait: 0)
|
|
||||||
expect(page).to have_css(".d-modal:not(.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 has_disabled_email?
|
|
||||||
find(".create-account-email").has_css?("input[disabled]")
|
|
||||||
end
|
|
||||||
|
|
||||||
def has_disabled_name?
|
|
||||||
find(".create-account__fullname").has_css?("input[disabled]")
|
|
||||||
end
|
|
||||||
|
|
||||||
def has_disabled_username?
|
|
||||||
find(".create-account__username").has_css?("input[disabled]")
|
|
||||||
end
|
|
||||||
|
|
||||||
def click_social_button(provider)
|
|
||||||
click(".btn-social.#{provider}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,8 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
shared_examples "signup scenarios" do |signup_page_object, login_page_object|
|
shared_examples "signup scenarios" do |signup_page_object, login_page_object|
|
||||||
let(:login_form) { login_page_object }
|
|
||||||
let(:signup_form) { signup_page_object }
|
let(:signup_form) { signup_page_object }
|
||||||
|
let(:login_form) { login_page_object }
|
||||||
let(:invite_form) { PageObjects::Pages::InviteForm.new }
|
let(:invite_form) { PageObjects::Pages::InviteForm.new }
|
||||||
let(:activate_account) { PageObjects::Pages::ActivateAccount.new }
|
let(:activate_account) { PageObjects::Pages::ActivateAccount.new }
|
||||||
let(:invite) { Fabricate(:invite, email: "johndoe@example.com") }
|
let(:invite) { Fabricate(:invite, email: "johndoe@example.com") }
|
||||||
|
@ -285,14 +285,8 @@ shared_examples "signup scenarios" do |signup_page_object, login_page_object|
|
||||||
|
|
||||||
expect(page).to have_css(".invited-by .user-info[data-username='#{inviter.username}']")
|
expect(page).to have_css(".invited-by .user-info[data-username='#{inviter.username}']")
|
||||||
find(".invitation-cta__sign-in").click
|
find(".invitation-cta__sign-in").click
|
||||||
|
expect(page).to have_css("#login-form")
|
||||||
if SiteSetting.full_page_login
|
page.go_back
|
||||||
expect(page).to have_css("#login-form")
|
|
||||||
page.go_back
|
|
||||||
else
|
|
||||||
find(".d-modal .modal-close").click
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(page).to have_css(".invited-by .user-info[data-username='#{inviter.username}']")
|
expect(page).to have_css(".invited-by .user-info[data-username='#{inviter.username}']")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -350,14 +344,12 @@ end
|
||||||
|
|
||||||
describe "Signup", type: :system do
|
describe "Signup", type: :system do
|
||||||
context "when fullpage desktop" do
|
context "when fullpage desktop" do
|
||||||
before { SiteSetting.full_page_login = true }
|
|
||||||
include_examples "signup scenarios",
|
include_examples "signup scenarios",
|
||||||
PageObjects::Pages::Signup.new,
|
PageObjects::Pages::Signup.new,
|
||||||
PageObjects::Pages::Login.new
|
PageObjects::Pages::Login.new
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when fullpage mobile", mobile: true do
|
context "when fullpage mobile", mobile: true do
|
||||||
before { SiteSetting.full_page_login = true }
|
|
||||||
include_examples "signup scenarios",
|
include_examples "signup scenarios",
|
||||||
PageObjects::Pages::Signup.new,
|
PageObjects::Pages::Signup.new,
|
||||||
PageObjects::Pages::Login.new
|
PageObjects::Pages::Login.new
|
||||||
|
|
|
@ -454,14 +454,12 @@ describe "Social authentication", type: :system do
|
||||||
before { SiteSetting.full_name_requirement = "optional_at_signup" }
|
before { SiteSetting.full_name_requirement = "optional_at_signup" }
|
||||||
|
|
||||||
context "when fullpage desktop" do
|
context "when fullpage desktop" do
|
||||||
before { SiteSetting.full_page_login = true }
|
|
||||||
include_examples "social authentication scenarios",
|
include_examples "social authentication scenarios",
|
||||||
PageObjects::Pages::Signup.new,
|
PageObjects::Pages::Signup.new,
|
||||||
PageObjects::Pages::Login.new
|
PageObjects::Pages::Login.new
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when fullpage mobile", mobile: true do
|
context "when fullpage mobile", mobile: true do
|
||||||
before { SiteSetting.full_page_login = true }
|
|
||||||
include_examples "social authentication scenarios",
|
include_examples "social authentication scenarios",
|
||||||
PageObjects::Pages::Signup.new,
|
PageObjects::Pages::Signup.new,
|
||||||
PageObjects::Pages::Login.new
|
PageObjects::Pages::Login.new
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue