diff --git a/apps/web/src/pages/change-password-page.tsx b/apps/web/src/pages/change-password-page.tsx index 6f912747..98023226 100644 --- a/apps/web/src/pages/change-password-page.tsx +++ b/apps/web/src/pages/change-password-page.tsx @@ -1,11 +1,40 @@ import { type FormEvent, useState } from "react"; +function generatePassword(): string { + const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const lower = "abcdefghijklmnopqrstuvwxyz"; + const digits = "0123456789"; + const all = upper + lower + digits; + // Guarantee at least one of each required character class + const required = [ + upper[Math.floor(Math.random() * upper.length)], + lower[Math.floor(Math.random() * lower.length)], + digits[Math.floor(Math.random() * digits.length)], + ]; + const rest = Array.from({ length: 13 }, () => all[Math.floor(Math.random() * all.length)]); + // Shuffle so the required chars aren't always at the start + const chars = [...required, ...rest]; + for (let i = chars.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [chars[i], chars[j]] = [chars[j], chars[i]]; + } + return chars.join(""); +} + export function ChangePasswordPage() { const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); + const [showGenerated, setShowGenerated] = useState(false); + + const handleGenerate = () => { + const pw = generatePassword(); + setNewPassword(pw); + setConfirmPassword(pw); + setShowGenerated(true); + }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); @@ -58,6 +87,13 @@ export function ChangePasswordPage() {

+ {/* Hidden username so the browser associates saved credentials correctly */} +
- +
+ + +
setNewPassword(e.target.value)} + onChange={(e) => { + setNewPassword(e.target.value); + setShowGenerated(false); + }} placeholder="At least 8 characters" - className="w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20" + className={`w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 ${showGenerated ? "font-mono text-sm" : ""}`} required minLength={8} /> @@ -102,11 +149,15 @@ export function ChangePasswordPage() { setConfirmPassword(e.target.value)} + onChange={(e) => { + setConfirmPassword(e.target.value); + setShowGenerated(false); + }} placeholder="Repeat new password" - className="w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20" + className={`w-full px-4 py-3 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 ${showGenerated ? "font-mono text-sm" : ""}`} required minLength={8} />