[object Object]

← back to Interiordesignershowroom

harden: intOrNull() guard on client-supplied ids reaching bigint columns

aae117fcdfddfd36476110747bbae04df3a85526 · 2026-08-03 10:32:52 -0700 · Steve

Non-numeric wall_paint_id (and similar) now coerces to null instead of
Number()->NaN reaching a bigint query and throwing a 500. Applied at
/api/rooms and /api/render call sites. (Pre-existing working-tree change,
snapshotted before deploy so shipped code == committed code.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit aae117fcdfddfd36476110747bbae04df3a85526
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Aug 3 10:32:52 2026 -0700

    harden: intOrNull() guard on client-supplied ids reaching bigint columns
    
    Non-numeric wall_paint_id (and similar) now coerces to null instead of
    Number()->NaN reaching a bigint query and throwing a 500. Applied at
    /api/rooms and /api/render call sites. (Pre-existing working-tree change,
    snapshotted before deploy so shipped code == committed code.)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 server.js | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/server.js b/server.js
index c555576..c0fa48e 100644
--- a/server.js
+++ b/server.js
@@ -20,6 +20,10 @@ app.use('/img', express.static(path.join(__dirname, 'public/img')));
 
 const { ROOMS } = require('./lib/nav'); // shared with render.js (nav) — one source of truth
 
+// Positive-integer id or null. Guards every place a client-supplied value reaches a
+// bigint column, so a non-numeric input 404s / no-ops instead of throwing a 500.
+const intOrNull = (v) => { const n = Number(v); return Number.isInteger(n) && n >= 1 ? n : null; };
+
 // --- tiny markdown-lite for guide bodies (headings, bold, paragraphs) ------
 function md(src = '') {
   return src.split(/\n\n+/).map((block) => {
@@ -377,7 +381,7 @@ app.post('/api/rooms', async (req, res, next) => {
     if (ids.length === 0) return res.status(400).json({ error: 'No products available to build a room.' });
     const slug = await rooms.createRoom({
       title: b.title, room_type: b.room_type, style: b.style,
-      wall_paint_id: b.wall_paint_id ? Number(b.wall_paint_id) : null,
+      wall_paint_id: intOrNull(b.wall_paint_id),
       product_ids: ids, note: b.note, scene_image: b.scene_image || null,
       hotspots: cleanSpots,
       created_by: b.created_by === 'curator' ? 'curator' : 'visitor',
@@ -467,7 +471,8 @@ app.post('/api/render', async (req, res) => {
       }
     }
     let wall = null;
-    if (b.wall_paint_id) { const w = (await db.query('SELECT title FROM products WHERE id=$1', [Number(b.wall_paint_id)])).rows[0]; wall = w && w.title; }
+    const wpId = intOrNull(b.wall_paint_id);
+    if (wpId) { const w = (await db.query('SELECT title FROM products WHERE id=$1', [wpId])).rows[0]; wall = w && w.title; }
     const out = await scene.generateScene({ style: b.style, color: b.color, theme: b.theme, period: b.period, room_type: b.room_type, wall, products: products.slice(0, 6) });
     // vision-locate the pieces so the builder render is shoppable too (paid Flash, ~$0.001)
     const loc = await hotspots.locateProducts(path.join(__dirname, 'public', out.url), products);

← 78c986b store: date + time added on product cards — New badge + 'Add  ·  back to Interiordesignershowroom  ·  robustness: bigint-safe id guard across all client-value->id 39571bf →