import { NextResponse } from "next/server"; import { cookies, headers } from "next/headers"; import { createRateLimiter, getClientIp } from "@/lib/rateLimit"; import { checkCsrf } from "@/lib/apiUtils"; const MEDUSA_URL = process.env.MEDUSA_URL ?? process.env.NEXT_PUBLIC_MEDUSA_URL ?? "http://localhost:9000"; const API_KEY = process.env.MEDUSA_PUBLISHABLE_KEY ?? process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY ?? ""; // 5 login attempts per IP per 15 minutes const limiter = createRateLimiter({ windowMs: 15 * 60 * 1000, maxRequests: 5 }); export async function POST(request: Request) { // CSRF check const csrfError = await checkCsrf(); if (csrfError) return csrfError; // Rate limit by IP const hdrs = await headers(); const ip = getClientIp(hdrs); const limit = limiter.check(`login:${ip}`); if (!limit.allowed) { return NextResponse.json( { error: "Too many login attempts. Please try again later." }, { status: 429, headers: { "Retry-After": String(Math.ceil(limit.retryAfterMs / 1000)) } }, ); } let body: Record; try { body = await request.json(); } catch { return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); } const { email, password } = body; // Server-side validation if (typeof email !== "string" || typeof password !== "string") { return NextResponse.json({ error: "Email and password are required" }, { status: 400 }); } const trimmedEmail = email.trim().toLowerCase(); if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmedEmail)) { return NextResponse.json({ error: "Invalid email address" }, { status: 400 }); } if (password.length < 8) { return NextResponse.json({ error: "Password must be at least 8 characters" }, { status: 400 }); } if (password.length > 128) { return NextResponse.json({ error: "Password too long" }, { status: 400 }); } // Authenticate with Medusa const authRes = await fetch(`${MEDUSA_URL}/auth/customer/emailpass`, { method: "POST", headers: { "Content-Type": "application/json", "x-publishable-api-key": API_KEY, }, body: JSON.stringify({ email: trimmedEmail, password }), }); if (!authRes.ok) { return NextResponse.json( { error: "Invalid email or password" }, { status: 401 }, ); } const { token } = await authRes.json(); // Set httpOnly cookie const cookieStore = await cookies(); cookieStore.set("medusa_auth_token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", maxAge: 604800, // 7 days }); // Fetch customer profile const meRes = await fetch(`${MEDUSA_URL}/store/customers/me`, { headers: { "x-publishable-api-key": API_KEY, Authorization: `Bearer ${token}`, }, }); if (!meRes.ok) { return NextResponse.json( { error: "Failed to fetch profile" }, { status: 500 }, ); } const { customer } = await meRes.json(); return NextResponse.json({ customer }); }