[object Object]

← back to Dw Hero Admin

dw-hero-admin: schema-validate /api/site/:site/grid POST (Tier 3 #17)

a1841b64ed5924f9c49682a9c1d6ab5c845343da · 2026-05-20 01:23:21 -0700 · SteveStudio2

Added validateGrid() — req.body must be {cells: [4 entries with non-empty http(s) image_url]}. Returns 400 + detail on violation. Closes the hardening gap where a malformed POST would write broken hero-4grid.json that pushToProd() then auto-rsyncs to prod fleet-wide.

Files touched

Diff

commit a1841b64ed5924f9c49682a9c1d6ab5c845343da
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 20 01:23:21 2026 -0700

    dw-hero-admin: schema-validate /api/site/:site/grid POST (Tier 3 #17)
    
    Added validateGrid() — req.body must be {cells: [4 entries with non-empty http(s) image_url]}. Returns 400 + detail on violation. Closes the hardening gap where a malformed POST would write broken hero-4grid.json that pushToProd() then auto-rsyncs to prod fleet-wide.
---
 server.js | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index 3aa9c45..05c0cfe 100644
--- a/server.js
+++ b/server.js
@@ -120,14 +120,33 @@ function defaultGrid(products) {
 }
 
 // ── save grid for a site ────────────────────────────────────────────────
+// Schema check — req.body must have `cells` array of length 4, each cell
+// containing image_url + (optional) overlay text. Prevents a malformed
+// payload from writing a broken hero-4grid.json that then gets rsynced to
+// prod by pushToProd() and breaks the live homepage hero.
+function validateGrid(body) {
+  if (!body || typeof body !== 'object') return 'body must be an object';
+  if (!Array.isArray(body.cells)) return 'body.cells must be an array';
+  if (body.cells.length !== 4) return `body.cells must have exactly 4 entries (got ${body.cells.length})`;
+  for (let i = 0; i < 4; i++) {
+    const c = body.cells[i];
+    if (!c || typeof c !== 'object') return `cells[${i}] must be an object`;
+    if (typeof c.image_url !== 'string' || !c.image_url) return `cells[${i}].image_url must be a non-empty string`;
+    if (!/^https?:\/\//.test(c.image_url)) return `cells[${i}].image_url must be http(s)`;
+  }
+  return null;
+}
+
 app.post('/api/site/:site/grid', (req, res) => {
   const site = req.params.site;
   if (!SITES.includes(site)) return res.status(404).json({ error: 'unknown site' });
+  const grid = req.body || {};
+  const validationError = validateGrid(grid);
+  if (validationError) return res.status(400).json({ error: 'invalid grid payload', detail: validationError });
   const projDir = path.join(PROJECTS_DIR, site);
   const pubDir = path.join(projDir, 'public');
   if (!fs.existsSync(pubDir)) fs.mkdirSync(pubDir, { recursive: true });
   const gridPath = path.join(pubDir, 'hero-4grid.json');
-  const grid = req.body || {};
   grid.updated_at = new Date().toISOString();
   fs.writeFileSync(gridPath, JSON.stringify(grid, null, 2));
   // Mirror to prod (non-blocking)

← 23d9290 fix: api() rejects on non-2xx HTTP so .catch handlers fire;  ·  back to Dw Hero Admin  ·  fix saveBtn dead-code r.ok check; add backup-file patterns t b35f88c →