// app/api/account/password/route.ts import { NextResponse } from "next/server"; import { requireBearer } from "@/app/api/_lib/auth"; import { loginDirectus } from "@/lib/directus"; export const runtime = "nodejs"; const API = (process.env.NEXT_PUBLIC_API_BASE_URL || process.env.DIRECTUS_URL || "").replace(/\/$/, ""); const bad = (m: string, c = 400, extra?: Record) => 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(() => ({} 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) 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 } } 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 bad("Current password is incorrect", 401); } // 5) Update password using ONLY the 'password' field const patchRes = await fetch(`${API}/users/me`, { method: "PATCH", headers: { Authorization: `Bearer ${bearer}`, "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ password: next }), }); const j = await patchRes.json().catch(() => ({})); if (!patchRes.ok) { const reason = j?.errors?.[0]?.message || j?.error || (typeof j === "string" ? j : "") || "Password change failed"; return bad(reason, patchRes.status); } return NextResponse.json({ ok: true }); } export async function POST(req: Request) { return handle(req); } export async function PATCH(req: Request) { return handle(req); }