import { NextRequest, NextResponse } from "next/server"; import { nanoid } from "nanoid"; import { eq } from "drizzle-orm"; import { db } from "@/lib/db"; import { claws } from "@/lib/db/schema"; import { redis } from "@/lib/redis"; import { generateApiKey } from "@/lib/auth/api-key"; import { getGeoLocation } from "@/lib/geo/ip-location"; import { applyDeterministicOffset } from "@/lib/geo/offset"; import { deviceRegisterSchema } 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"; } const RATE_LIMIT_KEY_PREFIX = "ratelimit:device:"; const RATE_LIMIT_MAX = 10; const RATE_LIMIT_WINDOW = 3600; // 1 hour export async function POST(req: NextRequest) { try { const clientIp = getClientIp(req); // Rate limit check const rateLimitKey = `${RATE_LIMIT_KEY_PREFIX}${clientIp}`; const currentCount = await redis.incr(rateLimitKey); if (currentCount === 1) { await redis.expire(rateLimitKey, RATE_LIMIT_WINDOW); } if (currentCount > RATE_LIMIT_MAX) { return NextResponse.json( { error: "Too many requests. Try again later." }, { status: 429 } ); } const body = await req.json(); const parsed = deviceRegisterSchema.safeParse(body); if (!parsed.success) { return NextResponse.json( { error: "Validation failed", details: parsed.error.flatten() }, { status: 400 } ); } const { deviceId, name, platform } = parsed.data; // Check if device already registered const existing = await db .select() .from(claws) .where(eq(claws.deviceId, deviceId)) .limit(1); if (existing.length > 0) { return NextResponse.json({ clawId: existing[0].id, apiKey: existing[0].apiKey, name: existing[0].name, isNew: false, }); } // Create new claw const clawId = nanoid(21); const apiKey = generateApiKey(); const geo = await getGeoLocation(clientIp); const now = new Date(); let finalLat: number | null = null; let finalLng: number | null = null; if (geo) { const offset = applyDeterministicOffset( geo.latitude, geo.longitude, clawId ); finalLat = offset.lat; finalLng = offset.lng; } // Auto-generate name if not provided const clawName = name || generateClawName(); await db.insert(claws).values({ id: clawId, apiKey, deviceId, name: clawName, platform: platform ? platform.slice(0, 20) : null, ip: clientIp, latitude: finalLat !== null ? String(finalLat) : null, longitude: finalLng !== null ? String(finalLng) : null, city: geo?.city ?? null, country: geo?.country ?? null, countryCode: geo?.countryCode ?? null, region: geo?.region ?? null, lastHeartbeat: now, totalTasks: 0, createdAt: now, updatedAt: now, }); return NextResponse.json({ clawId, apiKey, name: clawName, isNew: true, }); } catch (error) { console.error("Device register error:", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 } ); } } const ADJECTIVES = [ "Swift", "Brave", "Cool", "Dark", "Fire", "Ice", "Storm", "Shadow", "Cyber", "Neon", "Pixel", "Turbo", "Mega", "Ultra", "Nova", "Star", "Flash", "Thunder", "Iron", "Cosmic", ]; function generateClawName(): string { const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]; const num = Math.floor(Math.random() * 100); return `${adj}Claw-${num}`; }