Initial commit
**Motivations:** - Initialisation du versionning git pour le projet **Root causes:** - N/A (Nouveau projet) **Correctifs:** - N/A **Evolutions:** - Structure initiale du projet - Ajout du .gitignore **Pages affectées:** - Tous les fichiers
This commit is contained in:
228
server/routes/sales.js
Normal file
228
server/routes/sales.js
Normal file
@@ -0,0 +1,228 @@
|
||||
import express from "express";
|
||||
import {
|
||||
getAccountByPublicKey,
|
||||
getZooByAccountId,
|
||||
getSalesForZoo,
|
||||
getActiveSaleListings,
|
||||
createSaleListing,
|
||||
placeBid,
|
||||
acceptSale,
|
||||
rejectSale,
|
||||
markSaleDelivered,
|
||||
expireSaleListings,
|
||||
processValidatedSales,
|
||||
} 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);
|
||||
};
|
||||
}
|
||||
|
||||
function optionalSignature() {
|
||||
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) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
const ts = new Date(timestamp).getTime();
|
||||
if (Number.isNaN(ts) || Math.abs(now - ts) > TIMESTAMP_TOLERANCE_MS) {
|
||||
next();
|
||||
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)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
getAccountByPublicKey(publicKey).then((account) => {
|
||||
req.account = account ?? undefined;
|
||||
next();
|
||||
}).catch(next);
|
||||
};
|
||||
}
|
||||
|
||||
/** GET /api/sales — optional auth: with auth returns asSeller, asBuyerUndelivered, active; without auth returns active only. */
|
||||
router.get("/", optionalSignature(), async (req, res, next) => {
|
||||
try {
|
||||
await expireSaleListings();
|
||||
if (req.account) {
|
||||
await processValidatedSales();
|
||||
const zoo = await getZooByAccountId(req.account.id);
|
||||
if (!zoo) {
|
||||
res.json({ asSeller: [], asBuyerUndelivered: [], active: await getActiveSaleListings() });
|
||||
return;
|
||||
}
|
||||
const data = await getSalesForZoo(zoo.id);
|
||||
res.json(data);
|
||||
return;
|
||||
}
|
||||
const active = await getActiveSaleListings();
|
||||
res.json({ active });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
/** POST /api/sales — create listing (auth). Body: { animalId, isBaby, price, endAt, reproductionScoreAtSale? }. */
|
||||
router.post("/", requireSignature(), async (req, res, next) => {
|
||||
try {
|
||||
const zoo = await getZooByAccountId(req.account.id);
|
||||
if (!zoo) {
|
||||
res.status(404).json({ error: "No zoo for this account" });
|
||||
return;
|
||||
}
|
||||
const { animalId, isBaby, price, endAt, reproductionScoreAtSale } = req.body ?? {};
|
||||
if (typeof animalId !== "string" || !animalId.trim()) {
|
||||
res.status(400).json({ error: "animalId required" });
|
||||
return;
|
||||
}
|
||||
if (typeof isBaby !== "boolean") {
|
||||
res.status(400).json({ error: "isBaby boolean required" });
|
||||
return;
|
||||
}
|
||||
const initialPrice = Number(price);
|
||||
if (!Number.isFinite(initialPrice) || initialPrice < 0) {
|
||||
res.status(400).json({ error: "price must be a non-negative number" });
|
||||
return;
|
||||
}
|
||||
const endAtDate = endAt ? new Date(endAt) : null;
|
||||
if (!endAtDate || Number.isNaN(endAtDate.getTime())) {
|
||||
res.status(400).json({ error: "endAt required (ISO date string)" });
|
||||
return;
|
||||
}
|
||||
const { id } = await createSaleListing({
|
||||
sellerZooId: zoo.id,
|
||||
animalId: animalId.trim(),
|
||||
isBaby,
|
||||
initialPrice,
|
||||
endAt: endAtDate.toISOString(),
|
||||
reproductionScoreAtSale: reproductionScoreAtSale != null ? Number(reproductionScoreAtSale) : undefined,
|
||||
});
|
||||
res.status(201).json({ id });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
/** POST /api/sales/:id/bid — place bid (auth). Body: { amount }. */
|
||||
router.post("/:id/bid", requireSignature(), async (req, res, next) => {
|
||||
try {
|
||||
const zoo = await getZooByAccountId(req.account.id);
|
||||
if (!zoo) {
|
||||
res.status(404).json({ error: "No zoo for this account" });
|
||||
return;
|
||||
}
|
||||
const amount = Number(req.body?.amount);
|
||||
if (!Number.isFinite(amount) || amount < 0) {
|
||||
res.status(400).json({ error: "amount must be a non-negative number" });
|
||||
return;
|
||||
}
|
||||
const result = await placeBid(req.params.id, zoo.id, amount);
|
||||
if (!result.ok) {
|
||||
res.status(400).json({ error: result.reason ?? "Bid failed" });
|
||||
return;
|
||||
}
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
/** POST /api/sales/:id/accept — seller accepts best bid (auth). */
|
||||
router.post("/:id/accept", requireSignature(), async (req, res, next) => {
|
||||
try {
|
||||
const zoo = await getZooByAccountId(req.account.id);
|
||||
if (!zoo) {
|
||||
res.status(404).json({ error: "No zoo for this account" });
|
||||
return;
|
||||
}
|
||||
const result = await acceptSale(req.params.id, zoo.id);
|
||||
if (!result.ok) {
|
||||
res.status(400).json({ error: result.reason ?? "Accept failed" });
|
||||
return;
|
||||
}
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
/** POST /api/sales/:id/reject — seller rejects current best bid (auth). */
|
||||
router.post("/:id/reject", requireSignature(), async (req, res, next) => {
|
||||
try {
|
||||
const zoo = await getZooByAccountId(req.account.id);
|
||||
if (!zoo) {
|
||||
res.status(404).json({ error: "No zoo for this account" });
|
||||
return;
|
||||
}
|
||||
const result = await rejectSale(req.params.id, zoo.id);
|
||||
if (!result.ok) {
|
||||
res.status(400).json({ error: result.reason ?? "Reject failed" });
|
||||
return;
|
||||
}
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
/** POST /api/sales/:id/deliver — buyer marks as delivered (auth). */
|
||||
router.post("/:id/deliver", requireSignature(), async (req, res, next) => {
|
||||
try {
|
||||
const zoo = await getZooByAccountId(req.account.id);
|
||||
if (!zoo) {
|
||||
res.status(404).json({ error: "No zoo for this account" });
|
||||
return;
|
||||
}
|
||||
const result = await markSaleDelivered(req.params.id, zoo.id);
|
||||
if (!result.ok) {
|
||||
res.status(400).json({ error: result.reason ?? "Deliver failed" });
|
||||
return;
|
||||
}
|
||||
res.json({ ok: true });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
143
server/routes/zoos.js
Normal file
143
server/routes/zoos.js
Normal file
@@ -0,0 +1,143 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user