TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import { cn } from "@/lib/utils";3import { PASSWORD_MIN } from "./auth-errors";45const COMMON = ["password", "qwerty", "letmein", "welcome", "iloveyou", "admin", "monkey", "dragon", "polyllm", "abc123", "123456"];67/** Client-side heuristic (0–4). Not a substitute for server-side policy; it only guides the user. */8export function scorePassword(pw: string): { score: 0 | 1 | 2 | 3 | 4; label: string; hint: string } {9 if (!pw) return { score: 0, label: "", hint: `At least ${PASSWORD_MIN} characters. Long and memorable beats short and clever.` };10 if (pw.length < PASSWORD_MIN) return { score: 0, label: "Too short", hint: `${PASSWORD_MIN - pw.length} more character${PASSWORD_MIN - pw.length === 1 ? "" : "s"} to go.` };1112 let points = 0;13 const lower = /[a-z]/.test(pw);14 const upper = /[A-Z]/.test(pw);15 const digit = /\d/.test(pw);16 const symbol = /[^A-Za-z0-9]/.test(pw);17 const variety = [lower, upper, digit, symbol].filter(Boolean).length;1819 points += pw.length >= 14 ? 2 : 1;20 points += pw.length >= 20 ? 1 : 0;21 points += variety >= 3 ? 1 : 0;22 points += variety === 4 ? 1 : 0;2324 const lowered = pw.toLowerCase();25 if (COMMON.some((c) => lowered.includes(c))) points -= 2;26 if (/(.)\1{2,}/.test(pw)) points -= 1; // aaa27 if (/(0123|1234|2345|3456|4567|5678|6789|abcd|qwer|asdf)/i.test(pw)) points -= 1;28 if (new Set(pw).size <= Math.max(3, pw.length / 4)) points -= 1; // very low entropy2930 const score = Math.max(1, Math.min(4, points)) as 1 | 2 | 3 | 4;31 const labels: Record<1 | 2 | 3 | 4, [string, string]> = {32 1: ["Weak", "Add length or mix in words, numbers and symbols."],33 2: ["Fair", "A few more characters would make this much stronger."],34 3: ["Good", "Solid. Longer is always better."],35 4: ["Strong", "Excellent. Store it in a password manager."],36 };37 return { score, label: labels[score][0], hint: labels[score][1] };38}3940const COLORS = ["bg-border-strong", "bg-danger", "bg-warning", "bg-info", "bg-success"];41const TEXT = ["text-fg-subtle", "text-danger", "text-warning", "text-info", "text-success"];4243export function PasswordStrength({ password, id }: { password: string; id?: string }) {44 const { score, label, hint } = scorePassword(password);45 return (46 <div id={id} className="space-y-1.5" aria-live="polite">47 <div className="flex gap-1" role="meter" aria-valuemin={0} aria-valuemax={4} aria-valuenow={score} aria-label="Password strength">48 {[1, 2, 3, 4].map((i) => (49 <span key={i} className={cn("h-1 flex-1 rounded-full transition-colors duration-300", i <= score ? COLORS[score] : "bg-bg-muted")} />50 ))}51 </div>52 <p className="flex justify-between gap-3 text-xs">53 <span className="text-fg-subtle">{hint}</span>54 {label ? <span className={cn("shrink-0 font-medium", TEXT[score])}>{label}</span> : null}55 </p>56 </div>57 );58}59