more password change fixes
This commit is contained in:
parent
d090ff44f2
commit
c3fe52589f
2 changed files with 119 additions and 48 deletions
|
|
@ -5,59 +5,87 @@ import { loginDirectus } from "@/lib/directus";
|
|||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const API = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, "");
|
||||
const bad = (m: string, c = 400) => NextResponse.json({ error: m }, { status: c });
|
||||
const API = (process.env.NEXT_PUBLIC_API_BASE_URL || process.env.DIRECTUS_URL || "").replace(/\/$/, "");
|
||||
const bad = (m: string, c = 400, extra?: Record<string, any>) =>
|
||||
NextResponse.json(extra ? { error: m, ...extra } : { error: m }, { status: c });
|
||||
|
||||
function readCookie(req: Request, name: string): string | null {
|
||||
const cookie = req.headers.get("cookie") || "";
|
||||
const m = cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
|
||||
return m ? decodeURIComponent(m[1]) : null;
|
||||
}
|
||||
|
||||
function jwtPayload(token: string | null): any | null {
|
||||
if (!token) return null;
|
||||
try {
|
||||
const [, payload] = token.split(".");
|
||||
if (!payload) return null;
|
||||
const json = JSON.parse(
|
||||
Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8")
|
||||
);
|
||||
return json;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function handle(req: Request) {
|
||||
const bearer = requireBearer(req);
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const body = await req.json().catch(() => ({} as any));
|
||||
const current = String(body?.current ?? body?.current_password ?? "").trim();
|
||||
const next = String(body?.next ?? body?.new_password ?? "").trim();
|
||||
// NEW: allow client to provide identifier explicitly
|
||||
let identifier = String(body?.identifier ?? "").trim();
|
||||
|
||||
if (!current || !next) return bad("Missing current and/or new password");
|
||||
if (next.length < 8) return bad("Password must be at least 8 characters");
|
||||
|
||||
// 1) Load current user to get provider + identifiers
|
||||
const meRes = await fetch(`${API}/users/me?fields=id,email,username,provider`, {
|
||||
headers: { Authorization: `Bearer ${bearer}`, Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
const me = await meRes.json().catch(() => ({}));
|
||||
if (!meRes.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: "Could not verify user", debug: me?.errors?.[0]?.message || meRes.statusText },
|
||||
{ status: meRes.status }
|
||||
);
|
||||
}
|
||||
|
||||
const provider: string = me?.data?.provider ?? me?.provider ?? "local";
|
||||
const email: string | undefined = me?.data?.email ?? me?.email ?? undefined;
|
||||
const username: string | undefined = me?.data?.username ?? me?.username ?? undefined;
|
||||
|
||||
if (provider !== "local") {
|
||||
return NextResponse.json(
|
||||
{ error: "Password managed by external provider", debug: `provider=${provider}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 2) Verify CURRENT password by logging in with email OR username
|
||||
const identifier = email || username;
|
||||
// 1) Prefer client-provided identifier if present
|
||||
// (email or username — whatever your Directus login accepts)
|
||||
if (!identifier) {
|
||||
return NextResponse.json(
|
||||
{ error: "No login identifier available for this user", debug: "missing email and username" },
|
||||
{ status: 400 }
|
||||
);
|
||||
// 2) Try to read from /users/me (email or username), honoring role perms
|
||||
const meRes = await fetch(`${API}/users/me?fields=id,email,username,provider`, {
|
||||
headers: { Authorization: `Bearer ${bearer}`, Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
const me = await meRes.json().catch(() => ({}));
|
||||
if (meRes.ok) {
|
||||
const provider: string = me?.data?.provider ?? me?.provider ?? "local";
|
||||
if (provider !== "local") {
|
||||
return bad("Password managed by external provider", 400, { debug: `provider=${provider}` });
|
||||
}
|
||||
const email: string | undefined = me?.data?.email ?? me?.email ?? undefined;
|
||||
const username: string | undefined = me?.data?.username ?? me?.username ?? undefined;
|
||||
identifier = email || username || "";
|
||||
} else {
|
||||
// If we can’t read user fields (permissions), keep going
|
||||
}
|
||||
}
|
||||
|
||||
if (!identifier) {
|
||||
// 3) Try to decode JWT in ma_at — some Directus builds include email in JWT
|
||||
const token = readCookie(req, "ma_at");
|
||||
const claims = jwtPayload(token);
|
||||
const fromJwt = (claims?.email as string) || (claims?.user?.email as string) || "";
|
||||
identifier = fromJwt?.trim() || "";
|
||||
}
|
||||
|
||||
if (!identifier) {
|
||||
// Last resort: ask client to pass identifier next time
|
||||
return bad("No login identifier available for this user", 400, {
|
||||
debug: "missing email and username; pass `identifier` in request body",
|
||||
});
|
||||
}
|
||||
|
||||
// 4) Verify CURRENT password by logging in with identifier
|
||||
const auth = await loginDirectus(identifier, current).catch(() => null);
|
||||
const access = auth?.access_token ?? auth?.data?.access_token;
|
||||
if (!access) {
|
||||
return NextResponse.json({ error: "Current password is incorrect" }, { status: 401 });
|
||||
return bad("Current password is incorrect", 401);
|
||||
}
|
||||
|
||||
// 3) Update password using ONLY the 'password' field
|
||||
// 5) Update password using ONLY the 'password' field
|
||||
const patchRes = await fetch(`${API}/users/me`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
|
|
@ -72,7 +100,7 @@ async function handle(req: Request) {
|
|||
if (!patchRes.ok) {
|
||||
const reason =
|
||||
j?.errors?.[0]?.message || j?.error || (typeof j === "string" ? j : "") || "Password change failed";
|
||||
return NextResponse.json({ error: reason }, { status: patchRes.status });
|
||||
return bad(reason, patchRes.status);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
|
|
|
|||
|
|
@ -1,15 +1,44 @@
|
|||
// components/account/PasswordChange.tsx
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
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) {
|
||||
|
|
@ -20,26 +49,23 @@ export default function PasswordChange() {
|
|||
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 }),
|
||||
body: JSON.stringify({ current, next, identifier: identifier.trim() }),
|
||||
});
|
||||
|
||||
// ⬇️ Parse JSON **then** use debug if present
|
||||
const j = await r.json().catch(() => ({} as any));
|
||||
|
||||
if (!r.ok) {
|
||||
// ⬇️ This is the "debug block" so you can see the upstream reason
|
||||
setMsg(
|
||||
j?.error
|
||||
? j?.debug
|
||||
? `${j.error} (${j.debug})`
|
||||
: j.error
|
||||
: "Password change failed"
|
||||
);
|
||||
setMsg(j?.error ? (j?.debug ? `${j.error} (${j.debug})` : j.error) : "Password change failed");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -59,6 +85,20 @@ export default function PasswordChange() {
|
|||
<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
|
||||
|
|
@ -66,6 +106,7 @@ export default function PasswordChange() {
|
|||
type="password"
|
||||
value={current}
|
||||
onChange={(e) => setCurrent(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
|
@ -76,6 +117,7 @@ export default function PasswordChange() {
|
|||
type="password"
|
||||
value={next}
|
||||
onChange={(e) => setNext(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
|
@ -86,6 +128,7 @@ export default function PasswordChange() {
|
|||
type="password"
|
||||
value={next2}
|
||||
onChange={(e) => setNext2(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue