import { NextRequest, NextResponse } from "next/server"; import { SESSION_COOKIE, UserRole, authenticateUser, getDefaultPathForRole, serializeSession } from "@/lib/auth"; import { getRequestAuditContext, writeAuditTrail } from "@/lib/audit"; import { consumeRateLimit, getRateLimitHeaders } from "@/lib/rate-limit"; import { prisma } from "@/lib/prisma"; function getSafePath(value: string | null) { if (!value) { return null; } if (!value.startsWith("/")) { return null; } return value; } function resolveNumber(raw: string | undefined, fallback: number) { const value = Number(raw?.trim()); if (!Number.isInteger(value) || value <= 0) { return fallback; } return value; } export async function POST(request: NextRequest) { const { ipAddress, userAgent } = await getRequestAuditContext(); const retryControl = consumeRateLimit(ipAddress || "unknown", { scope: "auth_login", limit: resolveNumber(process.env.LOGIN_RATE_LIMIT_ATTEMPTS, 10), windowMs: resolveNumber(process.env.LOGIN_RATE_LIMIT_WINDOW_MS, 15 * 60 * 1000) }); if (!retryControl.allowed) { const loginUrl = new URL("/login", request.url); loginUrl.searchParams.set("error", "rate_limited"); const response = NextResponse.redirect(loginUrl); const headers = getRateLimitHeaders(retryControl); Object.entries(headers).forEach(([headerName, headerValue]) => { response.headers.set(headerName, headerValue); }); return response; } const form = await request.formData(); const rawEmail = form.get("email"); const rawPassword = form.get("password"); const rawNext = form.get("next"); const next = getSafePath(typeof rawNext === "string" ? rawNext : null); const email = typeof rawEmail === "string" ? rawEmail.trim() : ""; const password = typeof rawPassword === "string" ? rawPassword : ""; if (!email || !password) { const loginUrl = new URL("/login", request.url); loginUrl.searchParams.set("error", "credentials_required"); if (next) { loginUrl.searchParams.set("next", next); } return NextResponse.redirect(loginUrl); } const session = await authenticateUser(email, password); if (!session) { const attemptedUser = await prisma.user.findUnique({ where: { email }, select: { id: true, tenantId: true, status: true } }); if (attemptedUser) { await writeAuditTrail({ tenantId: attemptedUser.tenantId, actorUserId: attemptedUser.id, entityType: "user", entityId: attemptedUser.id, action: "user_login_failed", metadata: { email, status: attemptedUser.status, source: "web" }, ipAddress, userAgent }); } const loginUrl = new URL("/login", request.url); loginUrl.searchParams.set("error", "invalid_credentials"); if (next) { loginUrl.searchParams.set("next", next); } return NextResponse.redirect(loginUrl); } await prisma.user.update({ where: { id: session.userId }, data: { lastLoginAt: new Date() } }); await writeAuditTrail({ tenantId: session.tenantId, actorUserId: session.userId, entityType: "user", entityId: session.userId, action: "user_login", metadata: { email }, ipAddress, userAgent }); const destination = next ?? getDefaultPathForRole(session.role as UserRole); const response = NextResponse.redirect(new URL(destination, request.url)); response.cookies.set(SESSION_COOKIE, await serializeSession(session), { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", maxAge: Math.max(1, Math.floor(session.expiresAt - Date.now() / 1000)) }); return response; }