SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
2.8 KB · 59 lines tsx
Raw Blame History
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