DEV: use PasswordValidationHelper instead of mixin for signup controller (#31380)

This introduces a helper class for password validation logic, and
replaces the mixin in the signup controller class. All properties that
impact password validation in that class are also converted to
autotracked ones.
This commit is contained in:
Kelv
2025-02-20 10:01:28 +08:00
committed by GitHub
parent 29a8c6ee49
commit b8a4b11cbb
4 changed files with 135 additions and 54 deletions

View File

@ -14,25 +14,29 @@ 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 UsernameValidationHelper from "discourse/lib/username-validation-helper";
import { emailValid } from "discourse/lib/utilities";
import PasswordValidation from "discourse/mixins/password-validation";
import UserFieldsValidation from "discourse/mixins/user-fields-validation";
import { findAll } from "discourse/models/login-method";
import User from "discourse/models/user";
import { i18n } from "discourse-i18n";
export default class SignupPageController extends Controller.extend(
PasswordValidation,
UserFieldsValidation
) {
@service site;
@service siteSettings;
@service login;
@tracked accountName;
@tracked accountPassword;
@tracked accountEmail;
@tracked accountUsername;
@tracked isDeveloper = false;
@tracked authOptions;
@tracked skipConfirmation;
accountChallenge = 0;
accountHoneypot = 0;
formSubmitted = false;
@ -40,10 +44,10 @@ export default class SignupPageController extends Controller.extend(
prefilledUsername = null;
userFields = null;
maskPassword = true;
passwordValidationVisible = false;
emailValidationVisible = false;
nameValidationHelper = new NameValidationHelper(this);
usernameValidationHelper = new UsernameValidationHelper(this);
passwordValidationHelper = new PasswordValidationHelper(this);
@notEmpty("authOptions") hasAuthOptions;
@setting("enable_local_logins") canCreateLocal;
@ -53,7 +57,7 @@ export default class SignupPageController extends Controller.extend(
super.init(...arguments);
if (cookie("email")) {
this.set("accountEmail", cookie("email"));
this.accountEmail = cookie("email");
}
this.fetchConfirmationValue();
@ -64,6 +68,11 @@ export default class SignupPageController extends Controller.extend(
return this.usernameValidationHelper.usernameValidation;
}
@dependentKeyCompat
get passwordValidation() {
return this.passwordValidationHelper.passwordValidation;
}
get nameTitle() {
return this.nameValidationHelper.nameTitle;
}
@ -161,20 +170,8 @@ export default class SignupPageController extends Controller.extend(
);
}
@discourseComputed(
"passwordValidation.ok",
"passwordValidation.reason",
"passwordValidationVisible"
)
showPasswordValidation(
passwordValidationOk,
passwordValidationReason,
passwordValidationVisible
) {
return (
passwordValidationOk ||
(passwordValidationReason && passwordValidationVisible)
);
get showPasswordValidation() {
return this.passwordValidation.ok || this.passwordValidation.reason;
}
@discourseComputed("usernameValidation.reason")
@ -185,9 +182,8 @@ export default class SignupPageController extends Controller.extend(
);
}
@discourseComputed("authOptions.auth_provider")
passwordRequired(authProvider) {
return isEmpty(authProvider);
get passwordRequired() {
return isEmpty(this.authOptions?.auth_provider);
}
@discourseComputed
@ -243,15 +239,12 @@ export default class SignupPageController extends Controller.extend(
);
}
if (
this.get("authOptions.email") === email &&
this.get("authOptions.email_valid")
) {
if (this.authOptions?.email === email && this.authOptions?.email_valid) {
return EmberObject.create({
ok: true,
reason: i18n("user.email.authenticated", {
provider: this.authProviderDisplayName(
this.get("authOptions.auth_provider")
this.authOptions?.auth_provider
),
}),
});
@ -268,15 +261,6 @@ export default class SignupPageController extends Controller.extend(
this.accountUsername = event.target.value;
}
@action
togglePasswordValidation() {
if (this.passwordValidation.reason) {
this.set("passwordValidationVisible", true);
} else {
this.set("passwordValidationVisible", false);
}
}
@action
checkEmailAvailability() {
if (this.emailValidation.reason) {
@ -325,15 +309,10 @@ export default class SignupPageController extends Controller.extend(
});
}
@discourseComputed(
"accountEmail",
"authOptions.email",
"authOptions.email_valid"
)
emailDisabled() {
get emailDisabled() {
return (
this.get("authOptions.email") === this.accountEmail &&
this.get("authOptions.email_valid")
this.authOptions?.email === this.accountEmail &&
this.authOptions?.email_valid
);
}
@ -356,7 +335,7 @@ export default class SignupPageController extends Controller.extend(
}
if (
this.get("emailValidation.ok") &&
(isEmpty(this.accountUsername) || this.get("authOptions.email"))
(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,
@ -407,8 +386,8 @@ export default class SignupPageController extends Controller.extend(
handleSkipConfirmation() {
if (this.skipConfirmation) {
this.performAccountCreation().finally(() =>
this.set("skipConfirmation", false)
this.performAccountCreation().finally(
() => (this.skipConfirmation = false)
);
}
}
@ -433,7 +412,7 @@ export default class SignupPageController extends Controller.extend(
accountPasswordConfirm: this.accountHoneypot,
};
const destinationUrl = this.get("authOptions.destination_url");
const destinationUrl = this.authOptions?.destination_url;
if (!isEmpty(destinationUrl)) {
cookie("destination_url", destinationUrl, { path: "/" });
@ -485,7 +464,9 @@ export default class SignupPageController extends Controller.extend(
this.rejectedEmails.pushObject(result.values.email);
}
if (result.errors?.["user_password.password"]?.length > 0) {
this.rejectedPasswords.pushObject(attrs.accountPassword);
this.passwordValidationHelper.rejectedPasswords.push(
attrs.accountPassword
);
}
this.set("formSubmitted", false);
removeCookie("destination_url");
@ -534,7 +515,6 @@ export default class SignupPageController extends Controller.extend(
this.set("flash", "");
this.nameValidationHelper.forceValidationReason = true;
this.set("emailValidationVisible", true);
this.set("passwordValidationVisible", true);
const validation = [
this.emailValidation,

View File

@ -143,9 +143,7 @@ export default {
router.transitionTo("signup").then((signup) => {
const signupController =
signup.controller || owner.lookup("controller:signup");
Object.keys(createAccountProps || {}).forEach((key) => {
signupController.set(key, createAccountProps[key]);
});
Object.assign(signupController, createAccountProps);
signupController.handleSkipConfirmation();
});
} else {

View File

@ -0,0 +1,104 @@
import { tracked } from "@glimmer/tracking";
import { dependentKeyCompat } from "@ember/object/compat";
import { isEmpty } from "@ember/utils";
import { TrackedArray, TrackedMap } from "@ember-compat/tracked-built-ins";
import { i18n } from "discourse-i18n";
function failedResult(attrs) {
return {
failed: true,
ok: false,
element: document.querySelector("#new-account-password"),
...attrs,
};
}
function validResult(attrs) {
return { ok: true, ...attrs };
}
export default class PasswordValidationHelper {
@tracked rejectedPasswords = new TrackedArray();
@tracked rejectedPasswordsMessages = new TrackedMap();
constructor(owner) {
this.owner = owner;
}
get passwordInstructions() {
return i18n("user.password.instructions", {
count: this.passwordMinLength,
});
}
get passwordMinLength() {
return this.owner.admin || this.owner.isDeveloper
? this.owner.siteSettings.min_admin_password_length
: this.owner.siteSettings.min_password_length;
}
@dependentKeyCompat
get passwordValidation() {
if (!this.owner.passwordRequired) {
return validResult();
}
if (this.rejectedPasswords.includes(this.owner.accountPassword)) {
return failedResult({
reason:
this.rejectedPasswordsMessages.get(this.owner.accountPassword) ||
i18n("user.password.common"),
});
}
// If blank, fail without a reason
if (isEmpty(this.owner.accountPassword)) {
return failedResult({
message: i18n("user.password.required"),
reason: this.owner.forceValidationReason
? i18n("user.password.required")
: null,
});
}
// If too short
if (this.owner.accountPassword.length < this.passwordMinLength) {
return failedResult({
reason: i18n("user.password.too_short", {
count: this.passwordMinLength,
}),
});
}
if (
!isEmpty(this.owner.accountUsername) &&
this.owner.accountPassword === this.owner.accountUsername
) {
return failedResult({
reason: i18n("user.password.same_as_username"),
});
}
if (
!isEmpty(this.owner.accountName) &&
this.owner.accountPassword === this.owner.accountName
) {
return failedResult({
reason: i18n("user.password.same_as_name"),
});
}
if (
!isEmpty(this.owner.accountEmail) &&
this.owner.accountPassword === this.owner.accountEmail
) {
return failedResult({
reason: i18n("user.password.same_as_email"),
});
}
return validResult({
reason: i18n("user.password.ok"),
});
}
}

View File

@ -128,7 +128,6 @@
<div class="input-group create-account__password">
{{#if this.passwordRequired}}
<PasswordField
{{on "focusout" this.togglePasswordValidation}}
{{on "focusin" this.scrollInputIntoView}}
@value={{this.accountPassword}}
@capsLockOn={{this.capsLockOn}}
@ -160,7 +159,7 @@
class="more-info"
id="password-validation-more-info"
>
{{this.passwordInstructions}}
{{this.passwordValidationHelper.passwordInstructions}}
</span>
{{/if}}
<div