fix(web): password requirement indicators on registration and admin create-user
The /register checklist was dead markup: initPasswordValidation() was only ever called on the setup page, so the requirement rows never updated and the Register button stayed disabled forever — web self-registration was effectively broken. The register Alpine component now initializes it. The validator is parameterized (field/submit ids, confirm + username/ email checks optional) so the admin Create User form reuses it: confirm field, live requirement rows, and the Create button gated on all checks passing. Requirements mirror the server's passwordcomplex rules (8+ chars, upper/lower/number/special — the hardcoded defaults; server- tuned rule values are a future nicety, the server remains the enforcer either way).
This commit is contained in:
1 parent
f3c9d4b4e8
commit
bc6ce41762
7 files changed
+153
-124
No files matched your search
+99
-103
@@ -62,8 +62,8 @@ function updateRequirementStatus(elementId: string, passed: boolean): void {
|
||||
}
|
||||
}
|
||||
|
||||
function updateSubmitButton(allPassed: boolean): void {
|
||||
const button = document.getElementById("register-btn") as HTMLButtonElement;
|
||||
function updateSubmitButton(allPassed: boolean, submitId: string): void {
|
||||
const button = document.getElementById(submitId) as HTMLButtonElement;
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
@@ -77,118 +77,114 @@ function updateSubmitButton(allPassed: boolean): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Main validation orchestrator
|
||||
function validateAll(): void {
|
||||
const passwordField = document.getElementById("password") as HTMLInputElement;
|
||||
const confirmField = document.getElementById(
|
||||
"confirm-password",
|
||||
) as HTMLInputElement;
|
||||
const usernameField = document.getElementById("username") as HTMLInputElement;
|
||||
const emailField = document.getElementById("email") as HTMLInputElement;
|
||||
|
||||
if (!passwordField || !confirmField || !usernameField || !emailField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = passwordField.value;
|
||||
const confirm = confirmField.value;
|
||||
const username = usernameField.value;
|
||||
const email = emailField.value;
|
||||
|
||||
// Check password requirements
|
||||
const hasLen = hasMinimumLength(password);
|
||||
const hasUpper = hasUppercase(password);
|
||||
const hasLower = hasLowercase(password);
|
||||
const hasNum = hasNumber(password);
|
||||
const hasSpecial = hasSpecialChar(password);
|
||||
const doMatch = passwordsMatch(password, confirm);
|
||||
const hasUser = hasUsername(username);
|
||||
const hasEmailAddr = hasEmail(email);
|
||||
|
||||
// Update requirement indicators
|
||||
updateRequirementStatus("req-length", hasLen);
|
||||
updateRequirementStatus("req-upper", hasUpper);
|
||||
updateRequirementStatus("req-lower", hasLower);
|
||||
updateRequirementStatus("req-number", hasNum);
|
||||
updateRequirementStatus("req-special", hasSpecial);
|
||||
updateRequirementStatus("req-match", doMatch);
|
||||
|
||||
// Enable/disable submit button
|
||||
const allPassed =
|
||||
hasLen &&
|
||||
hasUpper &&
|
||||
hasLower &&
|
||||
hasNum &&
|
||||
hasSpecial &&
|
||||
doMatch &&
|
||||
hasUser &&
|
||||
hasEmailAddr;
|
||||
updateSubmitButton(allPassed);
|
||||
}
|
||||
|
||||
// Debounce function to avoid excessive validation calls
|
||||
let debounceTimer: number | null = null;
|
||||
|
||||
function debouncedValidation(): void {
|
||||
if (debounceTimer !== null) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
debounceTimer = window.setTimeout(() => {
|
||||
validateAll();
|
||||
debounceTimer = null;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Event handlers (PASSIVE - no preventDefault, doesn't block password managers)
|
||||
function onPasswordInput(): void {
|
||||
debouncedValidation();
|
||||
}
|
||||
|
||||
function onConfirmInput(): void {
|
||||
debouncedValidation();
|
||||
}
|
||||
|
||||
function onUsernameInput(): void {
|
||||
debouncedValidation();
|
||||
}
|
||||
|
||||
function onEmailInput(): void {
|
||||
debouncedValidation();
|
||||
}
|
||||
|
||||
// Initialization
|
||||
function initPasswordValidation(): void {
|
||||
const passwordField = document.getElementById("password") as HTMLInputElement;
|
||||
const confirmField = document.getElementById(
|
||||
"confirm-password",
|
||||
) as HTMLInputElement;
|
||||
const usernameField = document.getElementById("username") as HTMLInputElement;
|
||||
const emailField = document.getElementById("email") as HTMLInputElement;
|
||||
export interface PasswordValidationOptions {
|
||||
// Element ids. Defaults match the /register page; other forms (admin
|
||||
// create-user, setup) override what differs.
|
||||
passwordId?: string;
|
||||
confirmId?: string;
|
||||
usernameId?: string;
|
||||
emailId?: string;
|
||||
submitId?: string;
|
||||
requireUsernameEmail?: boolean;
|
||||
}
|
||||
|
||||
if (!passwordField || !confirmField || !usernameField || !emailField) {
|
||||
export function initPasswordValidation(
|
||||
options: PasswordValidationOptions = {},
|
||||
): void {
|
||||
const passwordId = options.passwordId ?? "password";
|
||||
const confirmId = options.confirmId ?? "confirm-password";
|
||||
const usernameId = options.usernameId ?? "username";
|
||||
const emailId = options.emailId ?? "email";
|
||||
const submitId = options.submitId ?? "register-btn";
|
||||
const requireUsernameEmail = options.requireUsernameEmail ?? true;
|
||||
|
||||
const passwordField = document.getElementById(passwordId) as HTMLInputElement;
|
||||
const confirmField = document.getElementById(confirmId) as HTMLInputElement;
|
||||
const usernameField = document.getElementById(usernameId) as HTMLInputElement;
|
||||
const emailField = document.getElementById(emailId) as HTMLInputElement;
|
||||
|
||||
if (!passwordField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validate = (): void => {
|
||||
const password = passwordField.value;
|
||||
const confirm = confirmField ? confirmField.value : password;
|
||||
const username = usernameField ? usernameField.value : "x";
|
||||
const email = emailField ? emailField.value : "x@x";
|
||||
|
||||
// Check password requirements
|
||||
const hasLen = hasMinimumLength(password);
|
||||
const hasUpper = hasUppercase(password);
|
||||
const hasLower = hasLowercase(password);
|
||||
const hasNum = hasNumber(password);
|
||||
const hasSpecial = hasSpecialChar(password);
|
||||
const doMatch = confirmField ? passwordsMatch(password, confirm) : true;
|
||||
const hasUser = !requireUsernameEmail || hasUsername(username);
|
||||
const hasEmailAddr = !requireUsernameEmail || hasEmail(email);
|
||||
|
||||
// Update requirement indicators
|
||||
updateRequirementStatus("req-length", hasLen);
|
||||
updateRequirementStatus("req-upper", hasUpper);
|
||||
updateRequirementStatus("req-lower", hasLower);
|
||||
updateRequirementStatus("req-number", hasNum);
|
||||
updateRequirementStatus("req-special", hasSpecial);
|
||||
updateRequirementStatus("req-match", doMatch);
|
||||
|
||||
// Enable/disable submit button
|
||||
const allPassed =
|
||||
hasLen && hasUpper && hasLower && hasNum && hasSpecial && doMatch && hasUser && hasEmailAddr;
|
||||
updateSubmitButton(allPassed, submitId);
|
||||
};
|
||||
|
||||
// Debounce to avoid excessive validation calls
|
||||
let debounceTimer: number | null = null;
|
||||
const debouncedValidation = (): void => {
|
||||
if (debounceTimer !== null) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
debounceTimer = window.setTimeout(() => {
|
||||
validate();
|
||||
debounceTimer = null;
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const onInput = (): void => {
|
||||
debouncedValidation();
|
||||
};
|
||||
|
||||
// Add passive event listeners - don't prevent default, don't block password managers
|
||||
passwordField.addEventListener("input", onPasswordInput, { passive: true });
|
||||
passwordField.addEventListener("paste", onPasswordInput, { passive: true });
|
||||
passwordField.addEventListener("input", onInput, { passive: true });
|
||||
passwordField.addEventListener("paste", onInput, { passive: true });
|
||||
|
||||
confirmField.addEventListener("input", onConfirmInput, { passive: true });
|
||||
confirmField.addEventListener("paste", onConfirmInput, { passive: true });
|
||||
|
||||
usernameField.addEventListener("input", onUsernameInput, { passive: true });
|
||||
usernameField.addEventListener("paste", onUsernameInput, { passive: true });
|
||||
|
||||
emailField.addEventListener("input", onEmailInput, { passive: true });
|
||||
emailField.addEventListener("paste", onEmailInput, { passive: true });
|
||||
if (confirmField) {
|
||||
confirmField.addEventListener("input", onInput, { passive: true });
|
||||
confirmField.addEventListener("paste", onInput, { passive: true });
|
||||
}
|
||||
if (usernameField) {
|
||||
usernameField.addEventListener("input", onInput, { passive: true });
|
||||
usernameField.addEventListener("paste", onInput, { passive: true });
|
||||
}
|
||||
if (emailField) {
|
||||
emailField.addEventListener("input", onInput, { passive: true });
|
||||
emailField.addEventListener("paste", onInput, { passive: true });
|
||||
}
|
||||
|
||||
// Initial validation
|
||||
validateAll();
|
||||
validate();
|
||||
}
|
||||
|
||||
// Export for use in template
|
||||
export { initPasswordValidation };
|
||||
|
||||
Alpine.data("validation", () => ({
|
||||
initPasswordValidation,
|
||||
}));
|
||||
|
||||
// Admin create-user form: one password field (no confirm), custom submit id.
|
||||
Alpine.data("adminCreateUser", () => ({
|
||||
init() {
|
||||
initPasswordValidation({
|
||||
confirmId: "cu-confirm-password",
|
||||
submitId: "create-user-submit",
|
||||
});
|
||||
},
|
||||
}));
|
||||
+7
-1
@@ -1,5 +1,6 @@
|
||||
import { Alpine } from "./alpine";
|
||||
import { applyTheme } from "./theme";
|
||||
import { initPasswordValidation } from "./password_validation";
|
||||
|
||||
function initRegisterTheme(): void {
|
||||
// Progressive enhancement: check localStorage immediately
|
||||
@@ -12,5 +13,10 @@ function initRegisterTheme(): void {
|
||||
export { initRegisterTheme };
|
||||
|
||||
Alpine.data("register", () => ({
|
||||
initRegisterTheme,
|
||||
init() {
|
||||
initRegisterTheme();
|
||||
// The requirements checklist + submit gating: without this the
|
||||
// checklist stayed dead markup and the Register button never enabled.
|
||||
initPasswordValidation();
|
||||
},
|
||||
}));
|
||||
Reference in new issue
Block a user