// Password validation for password-set forms (register, setup, admin // create-user, profile password change). The requirement checklist is // generated from the server's ACTIVE complexity configuration // (GET /api/auth/password-rules) so the UI mirrors what the server will // actually enforce — the server remains the enforcer; this is display // only. Procedural style, passive listeners (password-manager // compatibility). import { Alpine } from "./alpine"; interface PasswordRules { min_length: number; require_upper: boolean; require_lower: boolean; require_number: boolean; require_special: boolean; } const DEFAULT_RULES: PasswordRules = { min_length: 8, require_upper: true, require_lower: true, require_number: true, require_special: true, }; let activeRules: PasswordRules = { ...DEFAULT_RULES }; // Fetches the ACTIVE rules once per page load; falls back to the // historical defaults if the endpoint is unreachable. export async function loadPasswordRules(): Promise { try { const res = await fetch("/api/auth/password-rules"); if (!res.ok) throw new Error(String(res.status)); const rules = (await res.json()) as Partial; activeRules = { min_length: typeof rules.min_length === "number" && rules.min_length > 0 ? rules.min_length : DEFAULT_RULES.min_length, require_upper: rules.require_upper ?? DEFAULT_RULES.require_upper, require_lower: rules.require_lower ?? DEFAULT_RULES.require_lower, require_number: rules.require_number ?? DEFAULT_RULES.require_number, require_special: rules.require_special ?? DEFAULT_RULES.require_special, }; } catch { activeRules = { ...DEFAULT_RULES }; } return activeRules; } // Check functions (rules-driven) function checkLength(password: string, min: number): boolean { return password.length >= min; } 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 { return password.length > 0 && password === confirm; } function nonEmpty(value: string): boolean { return value.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, submitId: string): void { const button = document.getElementById(submitId) as HTMLButtonElement | null; if (!button) return; button.disabled = !allPassed; button.classList.toggle("opacity-50", !allPassed); button.classList.toggle("cursor-not-allowed", !allPassed); } function requirementRow(id: string, label: string): string { return `
  • ○ ${label}
  • `; } // (Re)generates the checklist rows from the ACTIVE rules inside the // container's