Files
bookhoard/web/src/password_validation.ts
T
john-okeefe 48eaa2d286 fix(alpine): wrap all Alpine.data() callbacks in arrow functions for proper component initialization
- Wrap all Alpine.data() object literals in arrow functions (() => ({}))
- This fixes "n.bind is not a function" errors when Alpine initializes components
- Alpine.data() requires a factory function, not a plain object
- Ensures each component instance gets its own closure and proper this binding

Fixed 24 TypeScript files:
- admin.ts, analytics.ts, api.ts, api-explorer-docs.ts
- bookshelf.ts, collection-rules.ts, collections.ts, conflicts.ts
- device-management.ts, docs.ts, header.ts, index.ts
- library.ts, linking.ts, login.ts, password_validation.ts
- profile-modal.ts, profile.ts, queue.ts, register.ts
- search.ts, theme.ts, toast-error.ts, toast.ts, unlinked_books.ts

Before: Alpine.data("name", { method1, method2 })
After:  Alpine.data("name", () => ({ method1, method2 }))

This is a critical fix for Alpine.js v3+ where components must be
registered as factory functions to ensure proper reactivity and
prevent binding errors during initialization.
2026-03-12 15:43:56 -04:00

195 lines
5.4 KiB
TypeScript

// Password validation for registration form
// Procedural style - no OOP, passive event listeners for password manager compatibility
import { Alpine } from "./alpine";
// Validation check functions
function hasMinimumLength(password: string): boolean {
return password.length >= 8;
}
function hasUppercase(password: string): boolean {
return /[A-Z]/.test(password);
}
function hasLowercase(password: string): boolean {
return /[a-z]/.test(password);
}
function hasNumber(password: string): boolean {
return /[0-9]/.test(password);
}
function hasSpecialChar(password: string): boolean {
return /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
}
function passwordsMatch(password: string, confirm: string): boolean {
if (!password && !confirm) {
return false;
}
return password === confirm;
}
function hasUsername(username: string): boolean {
return username.trim().length > 0;
}
function hasEmail(email: string): boolean {
return email.trim().length > 0;
}
// UI update functions
function updateRequirementStatus(elementId: string, passed: boolean): void {
const element = document.getElementById(elementId);
if (!element) {
return;
}
const icon = element.querySelector(".requirement-icon");
if (!icon) {
return;
}
if (passed) {
icon.textContent = "✓";
icon.className = "requirement-icon text-green-500";
element.style.color = "var(--text-primary)";
} else {
icon.textContent = "○";
icon.className = "requirement-icon";
element.style.color = "var(--text-secondary)";
}
}
function updateSubmitButton(allPassed: boolean): void {
const button = document.getElementById("register-btn") as HTMLButtonElement;
if (!button) {
return;
}
if (allPassed) {
button.disabled = false;
button.classList.remove("opacity-50", "cursor-not-allowed");
} else {
button.disabled = true;
button.classList.add("opacity-50", "cursor-not-allowed");
}
}
// 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;
if (!passwordField || !confirmField || !usernameField || !emailField) {
return;
}
// 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 });
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 });
// Initial validation
validateAll();
}
// Export for use in template
export { initPasswordValidation };
Alpine.data("validation", () => ({
initPasswordValidation,
}));