import { type FormEvent, useState } from "react"; import { formatHeaders } from "@/lib/api"; /** * Trigger the browser's "Save Password" prompt by submitting a real form * with the new credentials and causing a page navigation. * * Safari (and most browsers) only offer to save passwords when they detect: * 1. A real HTMLFormElement.submit() call (not fetch / XHR) * 2. Visible input fields with autocomplete="username" + "new-password" * 3. An actual page navigation following the submission * * We POST to "/" which the SPA serves as index.html. The browser sees the * form submission + navigation and prompts to save. */ function triggerBrowserPasswordSave(username: string, password: string) { const form = document.createElement("form"); form.method = "POST"; form.action = "/"; form.style.position = "fixed"; form.style.top = "-9999px"; const uField = document.createElement("input"); uField.type = "text"; uField.name = "username"; uField.autocomplete = "username"; uField.value = username; form.appendChild(uField); const pField = document.createElement("input"); pField.type = "password"; pField.name = "password"; pField.autocomplete = "new-password"; pField.value = password; form.appendChild(pField); document.body.appendChild(form); form.submit(); // The form.submit() causes a full page navigation to "/", so no cleanup needed. } 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(); setError(""); if (newPassword !== confirmPassword) { setError("Passwords do not match"); return; } setLoading(true); try { const res = await fetch("/api/auth/change-password", { method: "POST", headers: formatHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ currentPassword, newPassword }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); setError(data.error || "Failed to change password"); return; } // Trigger browser password save prompt via real form submission + navigation const username = localStorage.getItem("snapotter-username") || "admin"; triggerBrowserPasswordSave(username, newPassword); return; // navigation happens inside triggerBrowserPasswordSave } catch { setError("Connection error"); } finally { setLoading(false); } }; return (
You need to set a new password before continuing. Your password must be at least 8 characters with uppercase, lowercase, and a number.
Set a strong password to secure your account, then you are good to go.