profile editor bug fixes

This commit is contained in:
makearmy 2025-09-30 20:36:26 -04:00
parent 0cbeca833f
commit 16ae6d9c1c
4 changed files with 194 additions and 122 deletions

View file

@ -33,12 +33,16 @@ async function handle(req: Request) {
const friendly = /invalid|credential|old_password|incorrect/i.test(reason)
? "Current password is incorrect"
: reason;
// Propagate upstream status (401/403/400…) so the UI can react.
return NextResponse.json({ error: friendly }, { status: res.status });
// Include upstream reason for debugging on the client
return NextResponse.json({ error: friendly, debug: reason }, { status: res.status });
}
return NextResponse.json({ ok: true });
}
export async function POST(req: Request) { return handle(req); }
export async function PATCH(req: Request) { return handle(req); }
export async function POST(req: Request) {
return handle(req);
}
export async function PATCH(req: Request) {
return handle(req);
}

View file

@ -8,10 +8,7 @@ const API = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, "");
const bad = (m: string, c = 400) => NextResponse.json({ error: m }, { status: c });
export async function GET() {
// show username (read-only) + editable fields
// allow missing email
try {
// caller is already gated by middleware; no token needed for GET here
return NextResponse.json({ ok: true });
} catch (e: any) {
return bad(e?.message || "Failed to load profile", e?.status || 500);
@ -30,14 +27,11 @@ export async function PATCH(req: Request) {
const cookie = req.headers.get("cookie") || "";
const hasRecentAuth = /(?:^|;\s*)ma_ra=1(?:;|$)/.test(cookie);
if (!hasRecentAuth) {
return NextResponse.json(
{ error: "Re-authentication required" },
{ status: 428 } // Precondition Required
);
return NextResponse.json({ error: "Re-authentication required" }, { status: 428 });
}
}
const payload: any = {};
const payload: Record<string, any> = {};
if (typeof (body as any).first_name === "string")
payload.first_name = (body as any).first_name.trim();
if (typeof (body as any).last_name === "string")
@ -46,9 +40,10 @@ export async function PATCH(req: Request) {
payload.location = (body as any).location.trim();
if ("email" in body) {
const e = String((body as any).email ?? "").trim();
payload.email = e ? e : null; // ← optional! blank clears it
payload.email = e ? e : null; // optional; blank clears it
}
// (password handled by /api/account/password)
if (!Object.keys(payload).length) return bad("No changes");
const r = await fetch(`${API}/users/me`, {
method: "PATCH",
@ -58,6 +53,21 @@ export async function PATCH(req: Request) {
const j = await r.json().catch(() => ({}));
if (!r.ok) return bad(j?.errors?.[0]?.message || "Update failed", r.status);
// Single-use recent-auth: clear ma_ra after sensitive success
if (wantsSensitive) {
const resp = NextResponse.json({ ok: true });
resp.cookies.set({
name: "ma_ra",
value: "",
httpOnly: false,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: 0,
});
return resp;
}
return NextResponse.json({ ok: true });
} catch (e: any) {
return bad(e?.message || "Unexpected error", e?.status || 500);

View file

@ -9,107 +9,121 @@ type Props = { nextPath?: string; reauth?: boolean };
export default function SignIn({ nextPath = "/portal", reauth = false }: Props) {
const router = useRouter();
const [identifier, setIdentifier] = useState(""); // email OR username
const [password, setPassword] = useState("");
const [showPassword, setShow] = useState(false);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState<string | null>(null);
const [password, setPassword] = useState("");
const [showPassword, setShow] = useState(false);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState<string | null>(null);
const onSubmit = useCallback(async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErr(null);
setLoading(true);
try {
const res = await fetch("/api/auth/login", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ identifier, password }),
});
const txt = await res.text();
let j: any = null; try { j = txt ? JSON.parse(txt) : null; } catch {}
if (!res.ok) {
throw new Error(j?.error || j?.message || `Sign-in failed (${res.status})`);
const onSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErr(null);
setLoading(true);
try {
const res = await fetch("/api/auth/login", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ identifier, password }),
});
const txt = await res.text();
let j: any = null;
try {
j = txt ? JSON.parse(txt) : null;
} catch {}
if (!res.ok) {
throw new Error(j?.error || j?.message || `Sign-in failed (${res.status})`);
}
// If this sign-in is being used as a re-auth step, set a short-lived 'recent auth' marker.
if (reauth && typeof document !== "undefined") {
document.cookie = `ma_ra=1; Max-Age=300; Path=/; SameSite=Lax${
process.env.NODE_ENV === "production" ? "; Secure" : ""
}`;
}
// Land where caller requested
router.replace(nextPath);
router.refresh();
} catch (e: any) {
setErr(e?.message || "Unable to sign in.");
} finally {
setLoading(false);
}
// Land where caller requested
router.replace(nextPath);
router.refresh();
} catch (e: any) {
setErr(e?.message || "Unable to sign in.");
} finally {
setLoading(false);
}
}, [identifier, password, nextPath, router]);
},
[identifier, password, nextPath, router, reauth]
);
return (
<div className="mx-auto max-w-md rounded-lg border p-6 space-y-4">
<div>
<h1 className="mb-1 text-2xl font-semibold">
{reauth ? "Re-authenticate" : "Sign In"}
</h1>
<h1 className="mb-1 text-2xl font-semibold">{reauth ? "Re-authenticate" : "Sign In"}</h1>
<p className="text-sm opacity-70">
{reauth
? "Please sign in again to continue."
: <>Use your email <em>or</em> username with your password.</>}
</p>
</div>
{reauth ? "Please sign in again to continue." : <>Use your email <em>or</em> username with your password.</>}
</p>
</div>
<form className="space-y-4" onSubmit={onSubmit}>
<div className="space-y-1">
<label className="text-sm font-medium">Email or Username</label>
<input
type="text"
autoComplete="username"
className="w-full rounded-md border px-3 py-2"
placeholder="you@example.com or your-handle"
value={identifier}
onChange={(e) => setIdentifier(e.currentTarget.value)}
required
/>
</div>
<form className="space-y-4" onSubmit={onSubmit}>
<div className="space-y-1">
<label className="text-sm font-medium">Email or Username</label>
<input
type="text"
autoComplete="username"
className="w-full rounded-md border px-3 py-2"
placeholder="you@example.com or your-handle"
value={identifier}
onChange={(e) => setIdentifier(e.currentTarget.value)}
required
/>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Password</label>
<button
type="button"
className="text-xs opacity-70 hover:opacity-100"
onClick={() => setShow((s) => !s)}
>
{showPassword ? "Hide" : "Show"}
</button>
</div>
<input
type={showPassword ? "text" : "password"}
autoComplete="current-password"
className="w-full rounded-md border px-3 py-2"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
required
/>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Password</label>
<button
type="button"
className="text-xs opacity-70 hover:opacity-100"
onClick={() => setShow((s) => !s)}
>
{showPassword ? "Hide" : "Show"}
</button>
</div>
<input
type={showPassword ? "text" : "password"}
autoComplete="current-password"
className="w-full rounded-md border px-3 py-2"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
required
/>
</div>
{err && (
<div className="rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700">
{err}
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full rounded-md bg-black px-3 py-2 text-white disabled:opacity-60"
>
{loading ? "Signing in…" : reauth ? "Re-authenticate" : "Sign In"}
</button>
</form>
{!reauth && (
<div className="mt-2 text-center text-sm">
<span className="opacity-70">New here?</span>{" "}
<a className="underline" href="/auth/sign-up">Create an account</a>
</div>
)}
{err && (
<div className="rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700">
{err}
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full rounded-md bg-black px-3 py-2 text-white disabled:opacity-60"
>
{loading ? "Signing in…" : reauth ? "Re-authenticate" : "Sign In"}
</button>
</form>
{!reauth && (
<div className="mt-2 text-center text-sm">
<span className="opacity-70">New here?</span>{" "}
<a className="underline" href="/auth/sign-up">
Create an account
</a>
</div>
)}
</div>
);
}