requireArmaServer (src/lib/arma-bridge/server.ts) wraps the shared bridge auth + game-servers lookup that every v1 route repeated; asJson (json.ts) narrows untrusted request values to what Payload's json field validation accepts (strings normalize to the fallback instead of failing the write). All four v1 routes now use both. Also document the ARMA_BRIDGE_API_KEY env var missed when the bridge routes landed.
83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { asJson } from "@/lib/arma-bridge/json";
|
|
import { requireArmaServer } from "@/lib/arma-bridge/server";
|
|
|
|
export const runtime = "nodejs";
|
|
export const dynamic = "force-dynamic";
|
|
|
|
interface IncomingEvent {
|
|
eventId: string;
|
|
server: number;
|
|
type: string;
|
|
occurredAt: string;
|
|
payload: ReturnType<typeof asJson>;
|
|
}
|
|
|
|
export async function POST(req: NextRequest) {
|
|
const auth = await requireArmaServer(req);
|
|
if (auth instanceof NextResponse) return auth;
|
|
const { payload, server } = auth;
|
|
|
|
const body = (await req.json().catch(() => null)) as { events?: unknown[] } | null;
|
|
if (!body?.events || !Array.isArray(body.events) || body.events.length > 100) {
|
|
return NextResponse.json(
|
|
{ error: "events must be an array containing at most 100 entries." },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Validate + normalize; entries missing a string id or type are rejected.
|
|
const valid: IncomingEvent[] = [];
|
|
let rejected = 0;
|
|
for (const rawEvent of body.events) {
|
|
const event = rawEvent as Record<string, unknown>;
|
|
if (!event || typeof event.id !== "string" || typeof event.type !== "string") {
|
|
rejected++;
|
|
continue;
|
|
}
|
|
valid.push({
|
|
eventId: `${server.serverId}:${event.id}`,
|
|
server: server.id,
|
|
type: event.type,
|
|
occurredAt:
|
|
typeof event.occurredAt === "string" ? event.occurredAt : new Date().toISOString(),
|
|
payload: asJson(event.payload),
|
|
});
|
|
}
|
|
|
|
// One batched lookup for events that already exist (idempotency by eventId).
|
|
const existing = valid.length
|
|
? await payload.find({
|
|
collection: "arma-sync-events",
|
|
where: { eventId: { in: valid.map((event) => event.eventId) } },
|
|
limit: valid.length,
|
|
overrideAccess: true,
|
|
})
|
|
: { docs: [] };
|
|
const seen = new Set(existing.docs.map((doc) => doc.eventId));
|
|
|
|
let accepted = 0;
|
|
let duplicates = 0;
|
|
for (const event of valid) {
|
|
// Also catches the same id repeated within this batch.
|
|
if (seen.has(event.eventId)) {
|
|
duplicates++;
|
|
continue;
|
|
}
|
|
try {
|
|
await payload.create({ collection: "arma-sync-events", data: event, overrideAccess: true });
|
|
seen.add(event.eventId);
|
|
accepted++;
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
if (/duplicate key|unique constraint/i.test(message)) {
|
|
// Unique-constraint race with a concurrent request — already stored.
|
|
duplicates++;
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({ ok: true, accepted, duplicates, rejected });
|
|
}
|