/** * @file pets.js (routes) * @description `/api/pets` — the mascot's custom pets (`server/lib/pets.js`): * the list plus the featured ones, a pet's sprite, importing a `.codex-pet.zip`, * uploading an image, installing a featured pet from codex-pets.net by its * fixed id, and deleting one. Every write is same-origin; no route takes a * URL or a path. * @author Nguyễn Ngọc Trí Vĩ */ "use strict"; const express = require("express"); const pets = require("../lib/pets"); const { sameOriginGuard } = require("../lib/same-origin"); const router = express.Router(); const STATUS = { ENOPET: 404, ETOOBIG: 413, ETOOMANY: 409, EFETCH: 502, EBADORIGIN: 403, }; function sendError(res, err) { if (err && typeof err.code === "string" && /^E[A-Z]+$/.test(err.code)) { return res .status(STATUS[err.code] || 400) .json({ error: { code: err.code, message: err.message } }); } return res .status(500) .json({ error: { code: "EINTERNAL", message: String(err?.message || err) } }); } router.get("/", (_req, res) => { try { res.json({ pets: pets.listPets(), featured: pets.featured(), sheet: pets.SHEET }); } catch (err) { sendError(res, err); } }); router.get("/:id/sprite", (req, res) => { try { const file = pets.spriteFile(req.params.id); res.set("Content-Type", file.type); res.set("X-Content-Type-Options", "nosniff"); res.set("Cache-Control", "private, max-age=3600"); res.sendFile(file.path); } catch (err) { sendError(res, err); } }); const raw = (limit) => express.raw({ type: () => true, limit }); router.post("/import", sameOriginGuard, raw(pets.MAX_BUNDLE_BYTES), (req, res) => { try { res.status(201).json({ pet: pets.importBundle(req.body) }); } catch (err) { sendError(res, err); } }); router.post("/upload", sameOriginGuard, raw(pets.MAX_IMAGE_BYTES), (req, res) => { try { res.status(201).json({ pet: pets.addImage(req.body, req.query.name) }); } catch (err) { sendError(res, err); } }); router.post("/featured/:id", sameOriginGuard, async (req, res) => { try { res.status(201).json({ pet: await pets.installFeatured(req.params.id) }); } catch (err) { sendError(res, err); } }); router.delete("/:id", sameOriginGuard, (req, res) => { try { pets.removePet(req.params.id); res.json({ ok: true }); } catch (err) { sendError(res, err); } }); // An upload past its limit reaches here as express.raw's 413. router.use((err, _req, res, next) => { if (err && err.type === "entity.too.large") return res.status(413).json({ error: { code: "ETOOBIG", message: "the file is too large" } }); return next(err); }); module.exports = router;