import { NextResponse } from "next/server"; import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance/index"; import { getSettings } from "@/lib/localDb"; import { SignJWT } from "jose"; import { cookies } from "next/headers"; import { ensurePersistentManagementPasswordHash, getStoredManagementPassword, verifyManagementPassword, } from "@/lib/auth/managementPassword"; import { loginSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { checkLoginGuard, clearLoginAttempts, recordLoginFailure } from "@/server/auth/loginGuard"; // SECURITY: No hardcoded fallback — JWT_SECRET must be configured. if (!process.env.JWT_SECRET) { console.error("[SECURITY] FATAL: JWT_SECRET is not set. Login authentication is disabled."); } function getJwtSecret(): Uint8Array { return new TextEncoder().encode(process.env.JWT_SECRET || ""); } // Test seam for cookie store injection without affecting runtime behavior. export const authRouteInternals = { getCookieStore: cookies, }; export async function POST(request) { const auditContext = getAuditRequestContext(request); try { // Fail-fast if JWT_SECRET is not configured if (!process.env.JWT_SECRET) { logAuditEvent({ action: "auth.login.misconfigured", actor: "system", target: "dashboard-auth", resourceType: "auth_session", status: "failed", ipAddress: auditContext.ipAddress || undefined, requestId: auditContext.requestId, metadata: { reason: "missing_jwt_secret" }, }); return NextResponse.json( { error: "Server misconfigured: JWT_SECRET not set. Contact administrator." }, { status: 500 } ); } let rawBody; try { rawBody = await request.json(); } catch { return NextResponse.json( { error: { message: "Invalid request", details: [{ field: "body", message: "Invalid JSON body" }], }, }, { status: 400 } ); } // Zod validation const validation = validateBody(loginSchema, rawBody); if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); } const password = typeof validation.data.password === "string" ? validation.data.password : ""; if (!password) { return NextResponse.json({ error: "Invalid password payload" }, { status: 400 }); } const settings = await getSettings(); const bruteForceEnabled = settings.bruteForceProtection !== false; const clientIp = auditContext.ipAddress || null; const guardCheck = checkLoginGuard(clientIp, { enabled: bruteForceEnabled }); if (!guardCheck.allowed) { logAuditEvent({ action: "auth.login.locked", actor: "anonymous", target: "dashboard-auth", resourceType: "auth_session", status: "failed", ipAddress: clientIp || undefined, requestId: auditContext.requestId, metadata: { retryAfterSeconds: guardCheck.retryAfterSeconds || 0 }, }); return NextResponse.json( { error: "Too many failed attempts. Try again later." }, { status: 429, headers: guardCheck.retryAfterSeconds ? { "Retry-After": String(guardCheck.retryAfterSeconds) } : {}, } ); } const passwordState = await ensurePersistentManagementPasswordHash({ settings, source: "auth.login", }); const storedHash = getStoredManagementPassword(passwordState.settings); if (!storedHash) { logAuditEvent({ action: "auth.login.setup_required", actor: "anonymous", target: "dashboard-auth", resourceType: "auth_session", status: "failed", ipAddress: auditContext.ipAddress || undefined, requestId: auditContext.requestId, metadata: { reason: "missing_persisted_password" }, }); return NextResponse.json( { error: "No password configured. Complete onboarding first.", needsSetup: true }, { status: 403 } ); } const isValid = await verifyManagementPassword(password, storedHash); if (isValid) { const forceSecureCookie = process.env.AUTH_COOKIE_SECURE === "true"; const forwardedProtoHeader = request.headers.get("x-forwarded-proto") || ""; const forwardedProto = forwardedProtoHeader.split(",")[0].trim().toLowerCase(); const isHttpsRequest = forwardedProto === "https" || request.nextUrl?.protocol === "https:"; const useSecureCookie = forceSecureCookie || isHttpsRequest; const token = await new SignJWT({ authenticated: true }) .setProtectedHeader({ alg: "HS256" }) .setExpirationTime("30d") .sign(getJwtSecret()); const cookieStore = await authRouteInternals.getCookieStore(); cookieStore.set("auth_token", token, { httpOnly: true, secure: useSecureCookie, sameSite: "lax", path: "/", }); logAuditEvent({ action: "auth.login.success", actor: "admin", target: "dashboard-auth", resourceType: "auth_session", status: "success", ipAddress: auditContext.ipAddress || undefined, requestId: auditContext.requestId, metadata: { hasStoredPassword: Boolean(storedHash), passwordMigrated: passwordState.migrated, secureCookie: useSecureCookie, }, }); clearLoginAttempts(clientIp); return NextResponse.json({ success: true }); } const failureDecision = recordLoginFailure(clientIp, { enabled: bruteForceEnabled }); logAuditEvent({ action: "auth.login.failed", actor: "anonymous", target: "dashboard-auth", resourceType: "auth_session", status: "failed", ipAddress: auditContext.ipAddress || undefined, requestId: auditContext.requestId, metadata: { reason: "invalid_password", lockedOut: failureDecision.allowed === false }, }); if (!failureDecision.allowed) { return NextResponse.json( { error: "Too many failed attempts. Try again later." }, { status: 429, headers: failureDecision.retryAfterSeconds ? { "Retry-After": String(failureDecision.retryAfterSeconds) } : {}, } ); } return NextResponse.json({ error: "Invalid password" }, { status: 401 }); } catch (error) { console.error("[AUTH] Login failed:", error); logAuditEvent({ action: "auth.login.error", actor: "system", target: "dashboard-auth", resourceType: "auth_session", status: "failed", ipAddress: auditContext.ipAddress || undefined, requestId: auditContext.requestId, metadata: { message: error instanceof Error ? error.message : "unknown_error", }, }); return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } }