From d4d93bc0e3516c471e22140af26260152e1ee927 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 22 Feb 2026 18:40:22 -0500 Subject: [PATCH] feat(auth): add interactive password requirements validation to registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add password requirements checklist with visual indicators (✓/○) - Implement real-time validation for length, case, numbers, special chars - Add confirm password field with matching validation - Disable submit button until all requirements are met - Add TypeScript client-side validation with password manager compatibility --- templates/register.templ | 43 +++++++- web/src/password_validation.ts | 177 +++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 web/src/password_validation.ts diff --git a/templates/register.templ b/templates/register.templ index c98f811..f502c0a 100644 --- a/templates/register.templ +++ b/templates/register.templ @@ -19,11 +19,11 @@ templ Register() {
- +
- +
@@ -33,11 +33,38 @@ templ Register() {
+
+

Password Requirements:

+
    +
  • + At least 8 characters +
  • +
  • + One uppercase letter +
  • +
  • + One lowercase letter +
  • +
  • + One number +
  • +
  • + One special character +
  • +
  • + Passwords match +
  • +
+
- +
-
@@ -48,6 +75,14 @@ templ Register() { Already have an account? Login

+ + } \ No newline at end of file diff --git a/web/src/password_validation.ts b/web/src/password_validation.ts new file mode 100644 index 0000000..36d0c6e --- /dev/null +++ b/web/src/password_validation.ts @@ -0,0 +1,177 @@ +// Password validation for registration form +// Procedural style - no OOP, passive event listeners for password manager compatibility + +// 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 +(window as any).initPasswordValidation = initPasswordValidation;