import express from "express"; import { getAccountByPublicKey, getZooByAccountId, createZoo, getMapParams, getAllZoos, countPlayerZoos, createBotZoo, updateLastSeen, updateZooGameState, } from "../db.js"; import { verifySignature, buildSignMessage, hashBody } from "../auth.js"; const router = express.Router(); const TIMESTAMP_TOLERANCE_MS = 5 * 60 * 1000; function requireSignature() { return (req, res, next) => { const publicKey = req.headers["x-public-key"]; const signature = req.headers["x-signature"]; const timestamp = req.headers["x-timestamp"]; if (!publicKey || !signature || !timestamp) { res.status(401).json({ error: "Missing X-Public-Key, X-Signature, or X-Timestamp" }); return; } const now = Date.now(); const ts = new Date(timestamp).getTime(); if (Number.isNaN(ts) || Math.abs(now - ts) > TIMESTAMP_TOLERANCE_MS) { res.status(401).json({ error: "Invalid or expired timestamp" }); return; } const body = req.bodyRaw !== undefined && req.bodyRaw !== null ? req.bodyRaw : ""; const bodyHash = hashBody(body); const path = req.originalUrl || req.baseUrl + req.path || req.path; const message = buildSignMessage(req.method, path, timestamp, bodyHash); if (!verifySignature(publicKey, signature, message)) { res.status(401).json({ error: "Invalid signature" }); return; } getAccountByPublicKey(publicKey).then((account) => { if (!account) { res.status(401).json({ error: "Unknown account" }); return; } req.account = account; next(); }).catch(next); }; } /** GET /api/zoos — list zoos for map (no auth). Ensures minZoos with bots. */ router.get("/", async (req, res, next) => { try { const params = await getMapParams(); let zoos = await getAllZoos(); await countPlayerZoos(); const need = Math.max(0, params.minZoos - zoos.length); for (let i = 0; i < need; i++) { const x = 10 + Math.random() * 80; const y = 10 + Math.random() * 80; const weights = { Basic: 1 + Math.floor(Math.random() * 2), Ocean: Math.floor(Math.random() * 2), Mountain: Math.floor(Math.random() * 2) }; await createBotZoo(x, y, weights); } if (need > 0) zoos = await getAllZoos(); const worldZoos = zoos.map((z) => ({ id: z.id, name: z.name, x: z.x, y: z.y, animalWeights: z.animal_weights, game_state: z.game_state ?? null, })); res.json({ worldZoos, mapWidth: params.mapWidth, mapHeight: params.mapHeight }); } catch (e) { next(e); } }); /** GET /api/zoos/me — my zoo + game_state (auth). */ router.get("/me", requireSignature(), (req, res, next) => { getZooByAccountId(req.account.id) .then((zoo) => { if (!zoo) { res.status(404).json({ error: "No zoo for this account" }); return; } updateLastSeen(req.account.id); res.json({ zooId: zoo.id, name: zoo.name, x: zoo.x, y: zoo.y, game_state: zoo.game_state, }); }) .catch(next); }); /** PATCH /api/zoos/me — update game_state (auth). Body: { game_state }. */ router.patch("/me", requireSignature(), (req, res, next) => { getZooByAccountId(req.account.id) .then((zoo) => { if (!zoo) { res.status(404).json({ error: "No zoo for this account" }); return; } const game_state = req.body?.game_state; if (game_state === null || game_state === undefined || typeof game_state !== "object") { res.status(400).json({ error: "game_state object required" }); return; } return updateZooGameState(zoo.id, game_state).then(() => { res.json({ ok: true }); }); }) .catch(next); }); /** POST /api/zoos/me — create my zoo (auth). Body: { name?, game_state }. */ router.post("/me", requireSignature(), (req, res, next) => { getZooByAccountId(req.account.id) .then((existing) => { if (existing) { res.status(409).json({ error: "Zoo already exists" }); return; } const name = req.body?.name?.trim() || req.account.pseudo; const game_state = req.body?.game_state; if (game_state === null || game_state === undefined || typeof game_state !== "object") { res.status(400).json({ error: "game_state object required" }); return; } const x = 25 + Math.random() * 50; const y = 25 + Math.random() * 50; return createZoo({ accountId: req.account.id, name, x, y, gameState: game_state }).then(({ id }) => { res.status(201).json({ zooId: id, name, x, y }); }); }) .catch(next); }); export default router;