"use client"; import { useState } from "react"; import { Loader2, Save, Shield } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { PasswordInput } from "@/components/ui/password-input"; import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardActions } from "@/components/settings/_card-actions"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { statusText, type StatusMessage, } from "@/components/settings/account-section/account-section-utils"; import { useTranslations } from "@/lib/i18n/context"; import { apiFetchRaw } from "@/lib/api/api-fetch"; import { recentProofErrorMessage, throwIfReproofRequired, useRecentProof, } from "@/components/settings/security-section/use-recent-proof"; /** * Change-password card. * * It used to sit at the bottom of Settings → Account, under the profile form * and the identity fields. Changing a password is not a profile edit — it is * the same decision as enrolling a second factor or adding a passkey, and * those live in Settings → Security. The card moved to sit with them so * "how I get into this account" reads as one place. * * The status hint stores the i18n KEY, not the translated string, so a locale * switch re-renders it in the new language — the settings-wide convention * `statusText` exists for. */ export function PasswordCard() { const { t } = useTranslations(); const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [saving, setSaving] = useState(false); const [msg, setMsg] = useState(null); const [msgType, setMsgType] = useState<"success" | "error" | null>(null); const [dialogOpen, setDialogOpen] = useState(false); const recentProof = useRecentProof({ description: t("settings.reproof.descriptionChange"), }); async function handleChangePassword(e: React.FormEvent) { e.preventDefault(); setSaving(true); setMsg(null); setMsgType(null); if (newPassword !== confirmPassword) { setMsg({ key: "settings.passwordMismatch" }); setMsgType("error"); setSaving(false); return; } try { // An account with a second factor changes its password only with that // factor proved within five minutes; the shared dialog collects it when // the server asks and the change is sent again. const body = JSON.stringify({ currentPassword, newPassword, confirmPassword, }); const res = await recentProof.run(async () => throwIfReproofRequired( await apiFetchRaw("/api/auth/password", { method: "POST", headers: { "Content-Type": "application/json" }, body, }), ), ); const json = await res.json(); if (!res.ok) { setMsg( json.error ? { text: json.error } : { key: "settings.savingError" }, ); setMsgType("error"); return; } setMsg({ key: "settings.passwordUpdated" }); setMsgType("success"); setCurrentPassword(""); setNewPassword(""); setConfirmPassword(""); } catch (err) { const message = recentProofErrorMessage(err, ""); if (message === null) return; setMsg(message ? { text: message } : { key: "common.networkError" }); setMsgType("error"); } finally { setSaving(false); } } return ( <> {recentProof.dialog} {/* The action is this card's only one, so it sits in the card's action row like every other primary. It stays full-width under `sm` — the German "Passwort ändern" is long enough that a right-hugging button used to break through the card's right border on a narrow phone. */} { setDialogOpen(open); if (!open) { setCurrentPassword(""); setNewPassword(""); setConfirmPassword(""); setMsg(null); setMsgType(null); } }} title={t("settings.passwordTitle")} description={t("settings.changePasswordDescription")} className="sm:max-w-xl" >
setCurrentPassword(e.target.value)} />
setNewPassword(e.target.value)} />
setConfirmPassword(e.target.value)} />
{msg && (

{statusText(msg, t)}

)}
); }