From c7511b98fcdbf19a345c008e357db10c945097cd Mon Sep 17 00:00:00 2001 From: makearmy Date: Sat, 27 Sep 2025 14:44:41 -0400 Subject: [PATCH] sign in with username only restored --- app/api/auth/login/route.ts | 106 ++++++++++++++++++----------------- app/auth/sign-in/sign-in.tsx | 27 +++++---- 2 files changed, 68 insertions(+), 65 deletions(-) diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index cc9d2450..aa054e84 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -1,75 +1,79 @@ // app/api/auth/login/route.ts import { NextRequest, NextResponse } from "next/server"; -import { setAuthCookies } from "@/lib/auth-cookies"; +import { emailForUsername, loginDirectus } from "@/lib/directus"; -const BASE = process.env.DIRECTUS_URL!; -const ADMIN_TOKEN = process.env.DIRECTUS_TOKEN_ADMIN_REGISTER || ""; +export const runtime = "nodejs"; -async function findEmailForIdentifier(identifier: string): Promise { - const id = (identifier || "").trim(); - if (!id) return null; - - // If it's an email, we're done. - if (id.includes("@")) return id; - - // Otherwise look up by username using the admin/registration token. - if (!ADMIN_TOKEN) return null; - - const res = await fetch( - `${BASE}/users?filter[username][_eq]=${encodeURIComponent(id)}&fields=id,email,username&limit=1`, - { headers: { Authorization: `Bearer ${ADMIN_TOKEN}`, Accept: "application/json" } } - ); - - const json: any = await res.json().catch(() => null); - return json?.data?.[0]?.email ?? null; -} +const secure = process.env.NODE_ENV === "production"; +/** + * Accepts any of: + * - { identifier: string, password: string } // email or username in `identifier` + * - { email: string, password: string } + * - { username: string, password: string } + * + * On success: sets HttpOnly "ma_at" cookie and returns { ok: true }. + */ export async function POST(req: NextRequest) { try { - const body = await req.json(); - const identifier = (body?.identifier ?? body?.email ?? "").trim(); - const password = body?.password ?? ""; + const body = await req.json().catch(() => ({} as any)); + const { password } = body as { identifier?: string; email?: string; username?: string; password?: string }; + let identifier = (body?.identifier ?? body?.email ?? body?.username ?? "").trim(); if (!identifier || !password) { return NextResponse.json({ error: "Missing credentials" }, { status: 400 }); } - const email = await findEmailForIdentifier(identifier); + // 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) { - return NextResponse.json({ error: "Account not found" }, { status: 401 }); + email = await emailForUsername(identifier); + if (!email) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } } - // Login to Directus - const loginRes = await fetch(`${BASE}/auth/login`, { - method: "POST", - headers: { "Content-Type": "application/json", Accept: "application/json" }, - body: JSON.stringify({ email, password }), - }); - const loginJson: any = await loginRes.json().catch(() => null); - if (!loginRes.ok) { - const msg = loginJson?.errors?.[0]?.message || loginRes.statusText; - return NextResponse.json({ error: msg }, { status: loginRes.status }); - } + // Login against Directus; helper returns { access_token, refresh_token?, expires? } in .data or root + const data = await loginDirectus(email, password); + + const access = + data?.access_token ?? data?.data?.access_token ?? null; + const expiresSec = + data?.expires ?? data?.data?.expires ?? null; - const tokens = loginJson?.data ?? loginJson ?? {}; - const access = tokens.access_token; - const refresh = tokens.refresh_token; if (!access) { - return NextResponse.json({ error: "Login failed (no token)" }, { status: 500 }); + return NextResponse.json({ error: "Invalid response from auth provider" }, { status: 502 }); } - // Fetch user profile for the client - const meRes = await fetch(`${BASE}/users/me?fields=id,email,username`, { - headers: { Authorization: `Bearer ${access}`, Accept: "application/json" }, - }); - const meJson: any = await meRes.json().catch(() => null); - const user = (meJson?.data ?? meJson) || {}; + 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; + + res.cookies.set({ + name: "ma_at", + value: access, + httpOnly: true, + sameSite: "lax", + secure, + path: "/", + maxAge, + }); - let res = NextResponse.json({ ok: true, user }); - // Persist auth cookies expected by the rest of the app - res = setAuthCookies(res as any, { access_token: access, refresh_token: refresh } as any, user); return res; } catch (err: any) { - return NextResponse.json({ error: err?.message || "Login error" }, { status: 500 }); + const message = + 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 }); } } diff --git a/app/auth/sign-in/sign-in.tsx b/app/auth/sign-in/sign-in.tsx index 588bcd50..7382267a 100644 --- a/app/auth/sign-in/sign-in.tsx +++ b/app/auth/sign-in/sign-in.tsx @@ -8,7 +8,7 @@ type Props = { nextPath?: string }; export default function SignIn({ nextPath = "/portal" }: Props) { const router = useRouter(); - const [email, setEmail] = useState(""); + const [identifier, setIdentifier] = useState(""); // email OR username const [password, setPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); const [loading, setLoading] = useState(false); @@ -24,7 +24,7 @@ export default function SignIn({ nextPath = "/portal" }: Props) { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", Accept: "application/json" }, - body: JSON.stringify({ email, password }), + body: JSON.stringify({ identifier, password }), }); const txt = await res.text(); @@ -36,30 +36,31 @@ export default function SignIn({ nextPath = "/portal" }: Props) { throw new Error(message); } - router.replace(nextPath); // ALWAYS /portal + // Always land on the portal in this new flow + router.replace(nextPath); router.refresh(); } catch (e: any) { setErr(e?.message || "Unable to sign in."); } finally { setLoading(false); } - }, [email, password, nextPath, router]); + }, [identifier, password, nextPath, router]); return (

Sign In

-

Welcome back! Enter your credentials to continue.

+

Use your email or username with your password.

- + setEmail(e.currentTarget.value)} + placeholder="you@example.com or your-handle" + value={identifier} + onChange={(e) => setIdentifier(e.currentTarget.value)} required />
@@ -103,9 +104,7 @@ export default function SignIn({ nextPath = "/portal" }: Props) {
New here?{" "} - - Create an account - + Create an account
);