2025-09-30 00:56:23 -04:00
|
|
|
|
// app/api/account/password/route.ts
|
|
|
|
|
|
import { NextResponse } from "next/server";
|
|
|
|
|
|
import { requireBearer } from "@/app/api/_lib/auth";
|
2025-09-30 20:55:19 -04:00
|
|
|
|
import { loginDirectus } from "@/lib/directus";
|
2025-09-30 00:56:23 -04:00
|
|
|
|
|
2025-09-30 09:29:44 -04:00
|
|
|
|
export const runtime = "nodejs";
|
|
|
|
|
|
|
2025-09-30 22:51:12 -04:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-30 09:29:44 -04:00
|
|
|
|
|
|
|
|
|
|
async function handle(req: Request) {
|
|
|
|
|
|
const bearer = requireBearer(req);
|
|
|
|
|
|
|
2025-09-30 22:51:12 -04:00
|
|
|
|
const body = await req.json().catch(() => ({} as any));
|
2025-09-30 17:04:37 -04:00
|
|
|
|
const current = String(body?.current ?? body?.current_password ?? "").trim();
|
|
|
|
|
|
const next = String(body?.next ?? body?.new_password ?? "").trim();
|
2025-09-30 22:51:12 -04:00
|
|
|
|
// NEW: allow client to provide identifier explicitly
|
|
|
|
|
|
let identifier = String(body?.identifier ?? "").trim();
|
2025-09-30 09:29:44 -04:00
|
|
|
|
|
|
|
|
|
|
if (!current || !next) return bad("Missing current and/or new password");
|
|
|
|
|
|
if (next.length < 8) return bad("Password must be at least 8 characters");
|
|
|
|
|
|
|
2025-09-30 22:51:12 -04:00
|
|
|
|
// 1) Prefer client-provided identifier if present
|
|
|
|
|
|
// (email or username — whatever your Directus login accepts)
|
|
|
|
|
|
if (!identifier) {
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2025-09-30 20:49:37 -04:00
|
|
|
|
}
|
2025-09-30 20:55:19 -04:00
|
|
|
|
|
2025-09-30 22:51:12 -04:00
|
|
|
|
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() || "";
|
2025-09-30 20:55:19 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 22:45:42 -04:00
|
|
|
|
if (!identifier) {
|
2025-09-30 22:51:12 -04:00
|
|
|
|
// 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",
|
|
|
|
|
|
});
|
2025-09-30 20:49:37 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 22:51:12 -04:00
|
|
|
|
// 4) Verify CURRENT password by logging in with identifier
|
2025-09-30 22:45:42 -04:00
|
|
|
|
const auth = await loginDirectus(identifier, current).catch(() => null);
|
2025-09-30 20:55:19 -04:00
|
|
|
|
const access = auth?.access_token ?? auth?.data?.access_token;
|
|
|
|
|
|
if (!access) {
|
2025-09-30 22:51:12 -04:00
|
|
|
|
return bad("Current password is incorrect", 401);
|
2025-09-30 20:55:19 -04:00
|
|
|
|
}
|
2025-09-30 20:49:37 -04:00
|
|
|
|
|
2025-09-30 22:51:12 -04:00
|
|
|
|
// 5) Update password using ONLY the 'password' field
|
2025-09-30 20:55:19 -04:00
|
|
|
|
const patchRes = await fetch(`${API}/users/me`, {
|
2025-09-30 09:29:44 -04:00
|
|
|
|
method: "PATCH",
|
|
|
|
|
|
headers: {
|
2025-09-30 17:04:37 -04:00
|
|
|
|
Authorization: `Bearer ${bearer}`,
|
2025-09-30 09:29:44 -04:00
|
|
|
|
"Content-Type": "application/json",
|
2025-09-30 20:49:37 -04:00
|
|
|
|
Accept: "application/json",
|
2025-09-30 09:29:44 -04:00
|
|
|
|
},
|
2025-09-30 20:55:19 -04:00
|
|
|
|
body: JSON.stringify({ password: next }),
|
2025-09-30 09:29:44 -04:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-30 20:55:19 -04:00
|
|
|
|
const j = await patchRes.json().catch(() => ({}));
|
|
|
|
|
|
if (!patchRes.ok) {
|
2025-09-30 20:49:37 -04:00
|
|
|
|
const reason =
|
2025-09-30 20:55:19 -04:00
|
|
|
|
j?.errors?.[0]?.message || j?.error || (typeof j === "string" ? j : "") || "Password change failed";
|
2025-09-30 22:51:12 -04:00
|
|
|
|
return bad(reason, patchRes.status);
|
2025-09-30 09:29:44 -04:00
|
|
|
|
}
|
2025-09-30 00:56:23 -04:00
|
|
|
|
|
2025-09-30 09:29:44 -04:00
|
|
|
|
return NextResponse.json({ ok: true });
|
2025-09-30 00:56:23 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 20:49:37 -04:00
|
|
|
|
export async function POST(req: Request) { return handle(req); }
|
|
|
|
|
|
export async function PATCH(req: Request) { return handle(req); }
|