makearmy-app/app/api/account/password/route.ts

110 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<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(() => ({} 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 cant 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); }