← 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 →