import { NextRequest, NextResponse } from "next/server"; import { eq } from "drizzle-orm"; import { db } from "@/lib/db"; import { lobsters, heartbeats } from "@/lib/db/schema"; import { setLobsterOnline, updateActiveLobsters, incrementHourlyActivity, publishEvent, } from "@/lib/redis"; import { validateApiKey } from "@/lib/auth/api-key"; import { getGeoLocation } from "@/lib/geo/ip-location"; import { heartbeatSchema } from "@/lib/validators/schemas"; function getClientIp(req: NextRequest): string { const forwarded = req.headers.get("x-forwarded-for"); if (forwarded) return forwarded.split(",")[0].trim(); const realIp = req.headers.get("x-real-ip"); if (realIp) return realIp.trim(); return "127.0.0.1"; } export async function POST(req: NextRequest) { try { const authHeader = req.headers.get("authorization"); if (!authHeader?.startsWith("Bearer ")) { return NextResponse.json( { error: "Missing or invalid authorization header" }, { status: 401 } ); } const apiKey = authHeader.slice(7); const lobster = await validateApiKey(apiKey); if (!lobster) { return NextResponse.json({ error: "Invalid API key" }, { status: 401 }); } let bodyData = {}; try { const rawBody = await req.text(); if (rawBody) bodyData = JSON.parse(rawBody); } catch { // Empty body is fine for heartbeat } const parsed = heartbeatSchema.safeParse(bodyData); if (!parsed.success) { return NextResponse.json( { error: "Validation failed", details: parsed.error.flatten() }, { status: 400 } ); } const clientIp = getClientIp(req); const now = new Date(); const updateFields: Record = { lastHeartbeat: now, ip: clientIp, updatedAt: now, }; if (clientIp !== lobster.ip) { const geo = await getGeoLocation(clientIp); if (geo) { updateFields.latitude = String(geo.latitude); updateFields.longitude = String(geo.longitude); updateFields.city = geo.city; updateFields.country = geo.country; updateFields.countryCode = geo.countryCode; updateFields.region = geo.region; } } if (parsed.data.name) updateFields.name = parsed.data.name; if (parsed.data.model) updateFields.model = parsed.data.model; if (parsed.data.platform) updateFields.platform = parsed.data.platform; await setLobsterOnline(lobster.id, clientIp); await updateActiveLobsters(lobster.id); await incrementHourlyActivity(); await db .update(lobsters) .set(updateFields) .where(eq(lobsters.id, lobster.id)); // Insert heartbeat record asynchronously db.insert(heartbeats) .values({ lobsterId: lobster.id, ip: clientIp, timestamp: now }) .then(() => {}) .catch((err: unknown) => console.error("Failed to insert heartbeat:", err)); await publishEvent({ type: "heartbeat", lobsterId: lobster.id, lobsterName: (updateFields.name as string) ?? lobster.name, city: (updateFields.city as string) ?? lobster.city, country: (updateFields.country as string) ?? lobster.country, }); return NextResponse.json({ ok: true, nextIn: 180 }); } catch (error) { console.error("Heartbeat error:", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 } ); } }