feat: 添加设备注册 API 并重构安装横幅组件
This commit is contained in:
150
app/api/v1/device/register/route.ts
Normal file
150
app/api/v1/device/register/route.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user