"use client"; /** * ChangePasswordModal — three-field "change my password" form rendered in * a centred portal dialog. Mirrors the AuthModal pattern (backdrop + * fixed-position card + Esc-to-close + body scroll-lock) so the look and * feel matches. * * Opens from /settings → Authentication → Change password. * * On success: closes itself, refreshes the user state via auth-provider's * setUser so the rest of the app sees the fresh JWT-backed user. The BFF * has already re-issued the session cookie, so the caller stays signed in * on this device. */ import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { X } from "lucide-react"; import { useAuth } from "@/components/auth/auth-provider"; import { auth, AuthError } from "@/lib/auth"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; export function ChangePasswordModal({ open, onClose, }: { open: boolean; onClose: () => void; }) { const { user, setUser } = useAuth(); // OAuth-only accounts have no existing password to verify against — the // modal renders two fields (new + confirm) instead of three. Backend // accepts current_password as optional in this case. const requireCurrent = user?.has_password ?? true; const [current, setCurrent] = useState(""); const [next, setNext] = useState(""); const [confirm, setConfirm] = useState(""); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); // Reset form whenever the modal closes — re-opening should start blank, // not show the previous attempt. useEffect(() => { if (open) return; setCurrent(""); setNext(""); setConfirm(""); setError(null); setSubmitting(false); }, [open]); // Esc-to-close. useEffect(() => { if (!open) return; function onKey(e: KeyboardEvent) { if (e.key === "Escape") onClose(); } document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open, onClose]); // Body scroll-lock while the modal is open. useEffect(() => { if (!open) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, [open]); if (!open || typeof document === "undefined") return null; async function onSubmit(e: React.SyntheticEvent) { e.preventDefault(); setError(null); if (next.length < 8) { setError("New password must be at least 8 characters."); return; } if (next !== confirm) { setError("New passwords don't match."); return; } setSubmitting(true); try { const updated = await auth.changePassword(current, next); setUser(updated); onClose(); } catch (err) { setError( err instanceof AuthError ? err.message : "Could not change password.", ); } finally { setSubmitting(false); } } return createPortal( <>

Change password

{requireCurrent ? (
setCurrent(e.target.value)} />
) : null}
setNext(e.target.value)} />

At least 8 characters.

setConfirm(e.target.value)} />
{error ? (

{error}

) : null}
, document.body, ); }