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