From b835529b77268a7ab164b16e863eba9a1945e4f7 Mon Sep 17 00:00:00 2001 From: makearmy Date: Tue, 30 Sep 2025 01:05:03 -0400 Subject: [PATCH] account auth fix for account management --- app/api/account/password/route.ts | 37 ++++++++++++------------ app/api/account/profile/route.ts | 48 +++++++++++++++++++++++++++++++ app/api/auth/login/route.ts | 40 ++++++++++---------------- 3 files changed, 81 insertions(+), 44 deletions(-) create mode 100644 app/api/account/profile/route.ts diff --git a/app/api/account/password/route.ts b/app/api/account/password/route.ts index 2cfb90a9..f8ffdde8 100644 --- a/app/api/account/password/route.ts +++ b/app/api/account/password/route.ts @@ -1,4 +1,6 @@ // app/api/account/password/route.ts +export const runtime = "nodejs"; + import { NextResponse } from "next/server"; import { requireBearer } from "@/app/api/_lib/auth"; @@ -8,34 +10,31 @@ function bad(msg: string, code = 400) { return NextResponse.json({ error: msg }, { status: code }); } -/** - * This uses the simplest approach: PATCH /users/me { password: NEW }. - * It requires your role to have Update permission on `directus_users` with - * filter id = $CURRENT_USER and field permission for `password`. - * If your Directus is configured to use a dedicated password endpoint instead, - * swap the implementation accordingly. - */ export async function POST(req: Request) { try { const bearer = requireBearer(req); - const { current_password, new_password } = await req.json().catch(() => ({})); - if (!new_password) return bad("Missing new_password"); + const body = await req.json().catch(() => ({})); - // Optional: you can verify current_password server-side by attempting a login - // to your auth endpoint; omitted here to keep it simple. + // accept various shapes: {next}, {new_password}, {password} + const nextPwd = + String(body?.next ?? body?.new_password ?? body?.password ?? "").trim(); - const r = await fetch(`${API}/users/me`, { + if (nextPwd.length < 8) return bad("Password must be at least 8 characters"); + + const res = await fetch(`${API}/users/me`, { method: "PATCH", headers: { Authorization: bearer, "Content-Type": "application/json" }, - body: JSON.stringify({ password: new_password }), + body: JSON.stringify({ password: nextPwd }), }); - const j = await r.json().catch(() => ({})); - if (!r.ok) { - const msg = j?.errors?.[0]?.message || "Password update failed"; - return bad(msg, r.status); + + const j = await res.json().catch(() => ({})); + if (!res.ok) { + return bad(j?.errors?.[0]?.message || "Password update failed", res.status); } - return NextResponse.json({ ok: true }); + + // tell the client to re-auth so their token reflects the password change + return NextResponse.json({ ok: true, requireReauth: true }); } catch (e: any) { - return bad(e?.message || "Failed to change password", e?.status || 500); + return bad(e?.message || "Unexpected error", e?.status || 500); } } diff --git a/app/api/account/profile/route.ts b/app/api/account/profile/route.ts new file mode 100644 index 00000000..8dfac024 --- /dev/null +++ b/app/api/account/profile/route.ts @@ -0,0 +1,48 @@ +// app/api/account/profile/route.ts +export const runtime = "nodejs"; + +import { NextResponse } from "next/server"; +import { requireBearer } from "@/app/api/_lib/auth"; + +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); + } +} + +export async function PATCH(req: Request) { + try { + const bearer = requireBearer(req); + const body = await req.json().catch(() => ({})); + + const payload: any = {}; + if (typeof body.first_name === "string") payload.first_name = body.first_name.trim(); + if (typeof body.last_name === "string") payload.last_name = body.last_name.trim(); + if (typeof body.location === "string") payload.location = body.location.trim(); + if ("email" in body) { + const e = String(body.email ?? "").trim(); + payload.email = e ? e : null; // ← optional! blank clears it + } + // (password handled by /api/account/password) + + const r = await fetch(`${API}/users/me`, { + method: "PATCH", + headers: { Authorization: bearer, "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + const j = await r.json().catch(() => ({})); + if (!r.ok) return bad(j?.errors?.[0]?.message || "Update failed", r.status); + + return NextResponse.json({ ok: true }); + } catch (e: any) { + return bad(e?.message || "Unexpected error", e?.status || 500); + } +} diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index aa054e84..37578f41 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -1,6 +1,6 @@ // app/api/auth/login/route.ts import { NextRequest, NextResponse } from "next/server"; -import { emailForUsername, loginDirectus } from "@/lib/directus"; +import { loginDirectus } from "@/lib/directus"; export const runtime = "nodejs"; @@ -8,7 +8,7 @@ const secure = process.env.NODE_ENV === "production"; /** * Accepts any of: - * - { identifier: string, password: string } // email or username in `identifier` + * - { identifier: string, password: string } // email OR username in `identifier` * - { email: string, password: string } * - { username: string, password: string } * @@ -17,26 +17,17 @@ const secure = process.env.NODE_ENV === "production"; export async function POST(req: NextRequest) { try { const body = await req.json().catch(() => ({} as any)); - const { password } = body as { identifier?: string; email?: string; username?: string; password?: string }; + const password = String(body?.password ?? "").trim(); + const identifier = String( + body?.identifier ?? body?.email ?? body?.username ?? "" + ).trim(); - let identifier = (body?.identifier ?? body?.email ?? body?.username ?? "").trim(); if (!identifier || !password) { return NextResponse.json({ error: "Missing credentials" }, { status: 400 }); } - // Resolve to an email for Directus login: - // - If identifier looks like an email, use it directly. - // - Otherwise treat it as a username and look up the email. - let email = identifier.includes("@") ? identifier : null; - if (!email) { - email = await emailForUsername(identifier); - if (!email) { - return NextResponse.json({ error: "User not found" }, { status: 404 }); - } - } - - // Login against Directus; helper returns { access_token, refresh_token?, expires? } in .data or root - const data = await loginDirectus(email, password); + // Directus accepts username in the "email" field for /auth/login + const data = await loginDirectus(identifier, password); const access = data?.access_token ?? data?.data?.access_token ?? null; @@ -44,17 +35,17 @@ export async function POST(req: NextRequest) { data?.expires ?? data?.data?.expires ?? null; if (!access) { - return NextResponse.json({ error: "Invalid response from auth provider" }, { status: 502 }); + return NextResponse.json( + { error: "Invalid response from auth provider" }, + { status: 502 } + ); } const res = NextResponse.json({ ok: true }); - // Set access token cookie - // - HttpOnly so JS can't read it - // - SameSite=Lax to allow normal navigation - // - Secure in production - // - Max-Age from Directus if provided; else fallback to 8h - const maxAge = typeof expiresSec === "number" ? Math.max(0, Math.floor(expiresSec)) : 60 * 60 * 8; + // Max-Age from Directus if provided; else fallback to 8h + const maxAge = + typeof expiresSec === "number" ? Math.max(0, Math.floor(expiresSec)) : 60 * 60 * 8; res.cookies.set({ name: "ma_at", @@ -72,7 +63,6 @@ export async function POST(req: NextRequest) { err?.response?.data?.error || err?.message || "Login failed"; - // Return 401 for auth problems; 400 for others const status = /unauth|invalid|credentials/i.test(message) ? 401 : 400; return NextResponse.json({ error: message }, { status }); }