import { NextResponse } from "next/server"; import type { LoginResponse } from "@duoc-thu/shared-types"; import { SESSION_COOKIE, SESSION_MAX_AGE_SECONDS } from "../session"; const GATEWAY_URL = process.env.API_GATEWAY_URL ?? "http://localhost:3000"; export async function POST(request: Request) { let username: string; let password: string; try { const body = await request.json(); username = typeof body?.username === "string" ? body.username : ""; password = typeof body?.password === "string" ? body.password : ""; } catch { return NextResponse.json({ error: "invalid_body" }, { status: 400 }); } if (!username || !password) { return NextResponse.json({ error: "missing_credentials" }, { status: 400 }); } let upstream: Response; try { upstream = await fetch(`${GATEWAY_URL}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), cache: "no-store", }); } catch { return NextResponse.json({ error: "gateway_unreachable" }, { status: 502 }); } if (!upstream.ok) { return NextResponse.json( { error: "invalid_credentials" }, { status: upstream.status === 401 ? 401 : 502 } ); } const data = (await upstream.json()) as LoginResponse; const response = NextResponse.json({ user: data.user }); // httpOnly: never readable by client-side JS (XSS can't exfiltrate it). // `secure` only outside local dev — Compose/k3s both terminate TLS in // front of `web`, so the cookie is only ever sent in the clear on // localhost, matching how every other secret in this repo treats // local vs. deployed differently. response.cookies.set(SESSION_COOKIE, data.token, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", maxAge: SESSION_MAX_AGE_SECONDS, }); return response; }