Create monorepo from known-good production state
This commit is contained in:
commit
c034824338
651 changed files with 120469 additions and 0 deletions
148
app/components/account/PasswordChange.tsx
Normal file
148
app/components/account/PasswordChange.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
// components/account/PasswordChange.tsx
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Me = {
|
||||
id: string;
|
||||
username?: string | null;
|
||||
email?: string | null;
|
||||
};
|
||||
|
||||
export default function PasswordChange() {
|
||||
const [current, setCurrent] = useState("");
|
||||
const [next, setNext] = useState("");
|
||||
const [next2, setNext2] = useState("");
|
||||
const [identifier, setIdentifier] = useState(""); // email OR username (sent to API)
|
||||
const [needIdentifier, setNeedIdentifier] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [msg, setMsg] = useState<string | null>(null);
|
||||
|
||||
// Try to auto-fill identifier from /api/account
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const r = await fetch("/api/account", { credentials: "include", cache: "no-store" });
|
||||
const j = await r.json().catch(() => ({}));
|
||||
const me: Me | undefined = j?.user ?? j?.data ?? undefined;
|
||||
const id = (me?.email || me?.username || "").trim();
|
||||
if (id) {
|
||||
setIdentifier(id);
|
||||
setNeedIdentifier(false);
|
||||
} else {
|
||||
setNeedIdentifier(true);
|
||||
}
|
||||
} catch {
|
||||
// If it fails, we'll let the user type it
|
||||
setNeedIdentifier(true);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onSave = async () => {
|
||||
setMsg(null);
|
||||
if (next !== next2) {
|
||||
setMsg("New passwords do not match.");
|
||||
return;
|
||||
}
|
||||
if (next.length < 8) {
|
||||
setMsg("Password must be at least 8 characters.");
|
||||
return;
|
||||
}
|
||||
if (!identifier.trim()) {
|
||||
setNeedIdentifier(true);
|
||||
setMsg("Please enter your email or username.");
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
try {
|
||||
const r = await fetch("/api/account/password", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ current, next, identifier: identifier.trim() }),
|
||||
});
|
||||
|
||||
const j = await r.json().catch(() => ({} as any));
|
||||
if (!r.ok) {
|
||||
setMsg(j?.error ? (j?.debug ? `${j.error} (${j.debug})` : j.error) : "Password change failed");
|
||||
return;
|
||||
}
|
||||
|
||||
setMsg("Password updated.");
|
||||
setCurrent("");
|
||||
setNext("");
|
||||
setNext2("");
|
||||
} catch (e: any) {
|
||||
setMsg(e?.message || "Password change failed");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="security" className="rounded-md border p-4 space-y-3">
|
||||
<h3 className="font-semibold">Change Password</h3>
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-3 text-sm">
|
||||
{needIdentifier && (
|
||||
<label className="grid gap-1 sm:col-span-2">
|
||||
<span className="opacity-60">Email or Username</span>
|
||||
<input
|
||||
className="rounded-md border px-2 py-1"
|
||||
type="text"
|
||||
value={identifier}
|
||||
onChange={(e) => setIdentifier(e.target.value)}
|
||||
placeholder="you@example.com or your-handle"
|
||||
autoComplete="username"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<label className="grid gap-1 sm:col-span-2">
|
||||
<span className="opacity-60">Current Password</span>
|
||||
<input
|
||||
className="rounded-md border px-2 py-1"
|
||||
type="password"
|
||||
value={current}
|
||||
onChange={(e) => setCurrent(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-1">
|
||||
<span className="opacity-60">New Password</span>
|
||||
<input
|
||||
className="rounded-md border px-2 py-1"
|
||||
type="password"
|
||||
value={next}
|
||||
onChange={(e) => setNext(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-1">
|
||||
<span className="opacity-60">Confirm New Password</span>
|
||||
<input
|
||||
className="rounded-md border px-2 py-1"
|
||||
type="password"
|
||||
value={next2}
|
||||
onChange={(e) => setNext2(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={onSave}
|
||||
disabled={busy}
|
||||
className="rounded-md bg-black text-white px-3 py-2 text-sm disabled:opacity-60"
|
||||
>
|
||||
{busy ? "Saving…" : "Update Password"}
|
||||
</button>
|
||||
{msg && <div className="text-sm opacity-80">{msg}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue