56 lines
1.9 KiB
TypeScript
56 lines
1.9 KiB
TypeScript
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;
|
|
}
|