mirror of
https://github.com/myronblair/kino-app
synced 2026-07-27 21:18:43 -05:00
Add self-service admin password change (requires current password)
This commit is contained in:
@@ -51,6 +51,9 @@ export default function Settings() {
|
||||
const [traktStatus, setTraktStatus] = useState({ connected: false, username: "" });
|
||||
const [traktModal, setTraktModal] = useState(null);
|
||||
const [show, setShow] = useState({});
|
||||
const [pw, setPw] = useState({ current_password: "", new_password: "", confirm: "" });
|
||||
const [pwShow, setPwShow] = useState(false);
|
||||
const [pwSaving, setPwSaving] = useState(false);
|
||||
|
||||
const load = async () => {
|
||||
const [{ data }, { data: ts }] = await Promise.all([api.get("/settings"), api.get("/trakt/status").catch(() => ({ data: { connected: false } }))]);
|
||||
@@ -97,6 +100,19 @@ export default function Settings() {
|
||||
await api.delete("/trakt/disconnect"); toast.success("Trakt disconnected"); load();
|
||||
};
|
||||
|
||||
const changePassword = async (e) => {
|
||||
e.preventDefault();
|
||||
if (pw.new_password.length < 8) { toast.error("New password must be at least 8 characters"); return; }
|
||||
if (pw.new_password !== pw.confirm) { toast.error("New passwords don't match"); return; }
|
||||
setPwSaving(true);
|
||||
try {
|
||||
await api.post("/auth/change-password", { current_password: pw.current_password, new_password: pw.new_password });
|
||||
toast.success("Password changed");
|
||||
setPw({ current_password: "", new_password: "", confirm: "" });
|
||||
} catch (err) { toast.error(err.response?.data?.detail || "Could not change password"); }
|
||||
finally { setPwSaving(false); }
|
||||
};
|
||||
|
||||
const enrichAll = async () => {
|
||||
if (!window.confirm("Sweep all movies with missing metadata and fill from TMDB? This may take a while.")) return;
|
||||
try { const { data } = await api.post("/tmdb/enrich-all"); toast.success(`Enriched ${data.enriched}, skipped ${data.skipped}, failed ${data.failed}`); }
|
||||
@@ -110,6 +126,49 @@ export default function Settings() {
|
||||
<span className="text-xs uppercase tracking-[0.3em] text-[#D9381E]">Admin</span>
|
||||
<h1 className="font-display text-5xl md:text-6xl font-black tracking-tighter text-white mt-3">Settings</h1>
|
||||
|
||||
<form onSubmit={changePassword} className="mt-12 space-y-4 pb-10 border-b border-[#222]" data-testid="change-password-form">
|
||||
<h2 className="font-display text-2xl font-bold text-white">Admin Password</h2>
|
||||
<p className="text-sm text-[#8A8A8A]">Requires your current password. Changing it locks out anyone who only had the old one.</p>
|
||||
<label className="block">
|
||||
<span className="text-[10px] uppercase tracking-[0.3em] text-[#8A8A8A]">Current password</span>
|
||||
<div className="relative mt-2">
|
||||
<input
|
||||
type={pwShow ? "text" : "password"}
|
||||
value={pw.current_password}
|
||||
onChange={(e) => setPw({ ...pw, current_password: e.target.value })}
|
||||
className="w-full bg-[#0F0F0F] border border-[#222] focus:border-[#D9381E] focus:outline-none text-white px-4 py-3 pr-12"
|
||||
data-testid="current-password"
|
||||
/>
|
||||
<button type="button" onClick={() => setPwShow(!pwShow)} className="absolute right-3 top-1/2 -translate-y-1/2 text-[#8A8A8A] hover:text-white">
|
||||
{pwShow ? <EyeOff size={16} strokeWidth={1.5} /> : <Eye size={16} strokeWidth={1.5} />}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-[10px] uppercase tracking-[0.3em] text-[#8A8A8A]">New password (min 8 characters)</span>
|
||||
<input
|
||||
type={pwShow ? "text" : "password"}
|
||||
value={pw.new_password}
|
||||
onChange={(e) => setPw({ ...pw, new_password: e.target.value })}
|
||||
className="mt-2 w-full bg-[#0F0F0F] border border-[#222] focus:border-[#D9381E] focus:outline-none text-white px-4 py-3"
|
||||
data-testid="new-password"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-[10px] uppercase tracking-[0.3em] text-[#8A8A8A]">Confirm new password</span>
|
||||
<input
|
||||
type={pwShow ? "text" : "password"}
|
||||
value={pw.confirm}
|
||||
onChange={(e) => setPw({ ...pw, confirm: e.target.value })}
|
||||
className="mt-2 w-full bg-[#0F0F0F] border border-[#222] focus:border-[#D9381E] focus:outline-none text-white px-4 py-3"
|
||||
data-testid="confirm-password"
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" disabled={pwSaving} className="bg-[#D9381E] hover:bg-[#ED4B32] disabled:opacity-60 text-white px-8 py-3 text-sm uppercase tracking-[0.2em]" data-testid="change-password-button">
|
||||
{pwSaving ? "Changing…" : "Change Password"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form onSubmit={save} className="mt-12 space-y-10" data-testid="settings-form">
|
||||
{/* TMDB */}
|
||||
<section>
|
||||
|
||||
Reference in New Issue
Block a user