From b9c5c22f8668948ecf6810c112d80e94385411bc Mon Sep 17 00:00:00 2001 From: makearmy Date: Tue, 30 Sep 2025 10:03:32 -0400 Subject: [PATCH] add reauth for account changes --- app/api/auth/reconfirm/route.ts | 65 ++++++++++++++++++++++++++ components/account/ConfirmIdentity.tsx | 63 +++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 app/api/auth/reconfirm/route.ts create mode 100644 components/account/ConfirmIdentity.tsx diff --git a/app/api/auth/reconfirm/route.ts b/app/api/auth/reconfirm/route.ts new file mode 100644 index 00000000..42d174f0 --- /dev/null +++ b/app/api/auth/reconfirm/route.ts @@ -0,0 +1,65 @@ +// app/api/auth/reconfirm/route.ts +import { NextRequest, NextResponse } from "next/server"; +import { emailForUsername, loginDirectus } from "@/lib/directus"; + +export const runtime = "nodejs"; +const secure = process.env.NODE_ENV === "production"; + +export async function POST(req: NextRequest) { + try { + const body = await req.json().catch(() => ({} as any)); + const identifier = String(body?.identifier ?? "").trim(); + const password = String(body?.password ?? "").trim(); + + if (!identifier || !password) { + return NextResponse.json({ error: "Missing credentials" }, { status: 400 }); + } + + // Resolve identifier -> email (username allowed) + let email = identifier.includes("@") ? identifier : await emailForUsername(identifier); + if (!email) return NextResponse.json({ error: "User not found" }, { status: 404 }); + + const auth = await loginDirectus(email, password); + const access = auth?.access_token ?? auth?.data?.access_token; + const expiresSec = auth?.expires ?? auth?.data?.expires; + + if (!access) { + return NextResponse.json({ error: "Invalid credentials" }, { status: 401 }); + } + + const res = NextResponse.json({ ok: true }); + + // Refresh the access token cookie + const maxAge = typeof expiresSec === "number" ? Math.max(0, Math.floor(expiresSec)) : 60 * 60 * 8; + res.cookies.set({ + name: "ma_at", + value: access, + httpOnly: true, + sameSite: "lax", + secure, + path: "/", + maxAge, + }); + + // Short-lived client-visible flag: “recently authenticated” + res.cookies.set({ + name: "ma_ra", + value: "1", + httpOnly: false, + sameSite: "lax", + secure, + path: "/", + maxAge: 5 * 60, // 5 minutes + }); + + return res; + } catch (err: any) { + const msg = + err?.response?.data?.errors?.[0]?.message || + err?.response?.data?.error || + err?.message || + "Re-auth failed"; + const status = /invalid|credential/i.test(msg) ? 401 : 400; + return NextResponse.json({ error: msg }, { status }); + } +} diff --git a/components/account/ConfirmIdentity.tsx b/components/account/ConfirmIdentity.tsx new file mode 100644 index 00000000..da423dcf --- /dev/null +++ b/components/account/ConfirmIdentity.tsx @@ -0,0 +1,63 @@ +// components/account/ConfirmIdentity.tsx +"use client"; +import { useState } from "react"; + +export default function ConfirmIdentity({ + defaultIdentifier, + onSuccess, +}: { + defaultIdentifier: string; // prefill with username or email you show on the page + onSuccess: () => void; // called after re-auth succeeds +}) { + const [open, setOpen] = useState(false); + const [identifier, setIdentifier] = useState(defaultIdentifier); + const [password, setPassword] = useState(""); + const [busy, setBusy] = useState(false); + const [err, setErr] = useState(null); + + async function submit() { + setBusy(true); + setErr(null); + try { + const res = await fetch("/api/auth/reconfirm", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ identifier, password }), + }); + const j = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(j?.error || "Failed"); + setOpen(false); + setPassword(""); + onSuccess(); // now do the sensitive call + } catch (e: any) { + setErr(e?.message || "Re-auth failed"); + } finally { + setBusy(false); + } + } + + return ( + <> + {/* wherever you need the step-up, render a button that opens this */} + + + {open && ( +
+
+

Confirm it’s you

+ + setIdentifier(e.target.value)} /> + + setPassword(e.target.value)} /> + {err &&
{err}
} +
+ + +
+
+
+ )} + + ); +}