feat: 添加设备注册 API 并重构安装横幅组件

This commit is contained in:
richarjiang
2026-03-24 09:18:51 +08:00
parent ab00443056
commit 71628d783d
13 changed files with 1524 additions and 150 deletions

View File

@@ -0,0 +1,40 @@
import { NextRequest, NextResponse } from "next/server";
import { eq } from "drizzle-orm";
import { db } from "@/lib/db";
import { claws } from "@/lib/db/schema";
import { authenticateRequest } from "@/lib/auth/request";
import { updateNameSchema } from "@/lib/validators/schemas";
export async function PUT(req: NextRequest) {
try {
const auth = await authenticateRequest(req);
if (auth instanceof NextResponse) {
return auth;
}
const { claw } = auth;
const body = await req.json();
const parsed = updateNameSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: "Validation failed", details: parsed.error.flatten() },
{ status: 400 }
);
}
const { name } = parsed.data;
await db
.update(claws)
.set({ name, updatedAt: new Date() })
.where(eq(claws.id, claw.id));
return NextResponse.json({ ok: true, name });
} catch (error) {
console.error("Update name error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View 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}`;
}