[object Object]

← back to Costa Rica

costa-rica: column-list all remaining SELECT * (fleet PII-leak lint) + injection-free LIMIT/OFFSET; 61/61 tests green — TK-10346

fd3d8d2cec96dc3cf0940edd7bf5a74621d7645a · 2026-08-07 15:57:04 -0700 · Steve

Files touched

Diff

commit fd3d8d2cec96dc3cf0940edd7bf5a74621d7645a
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Aug 7 15:57:04 2026 -0700

    costa-rica: column-list all remaining SELECT * (fleet PII-leak lint) + injection-free LIMIT/OFFSET; 61/61 tests green — TK-10346
---
 lib/payouts.js       |  4 +++-
 routes/app.js        | 18 +++++++++++++-----
 server.js            |  2 +-
 test/payouts.test.js |  4 ++--
 4 files changed, 19 insertions(+), 9 deletions(-)

diff --git a/lib/payouts.js b/lib/payouts.js
index b1b93b7..7529556 100644
--- a/lib/payouts.js
+++ b/lib/payouts.js
@@ -9,7 +9,9 @@ const { pool } = require('./db');
 const { getProvider } = require('./payments');
 
 async function createPayoutForBooking(bookingId) {
-  const { rows: [b] } = await pool.query(`SELECT * FROM bookings WHERE id=$1`, [bookingId]);
+  const { rows: [b] } = await pool.query(
+    `SELECT id, code, host_id, traveler_id, currency, total, platform_fee, host_payout, status
+       FROM bookings WHERE id=$1`, [bookingId]);
   if (!b) throw new Error('booking not found');
   if (!b.host_id) throw new Error('booking has no host');
   if (b.status !== 'completed') throw new Error(`booking not completed (${b.status})`);
diff --git a/routes/app.js b/routes/app.js
index 1b7699d..31fcc76 100644
--- a/routes/app.js
+++ b/routes/app.js
@@ -107,6 +107,7 @@ router.get('/listings', optionalAuth, async (req, res) => {
   // Clamp to safe integers and inline — guaranteed numeric, no injection surface.
   const lim = Math.max(1, Math.min(parseInt(limit, 10) || 40, 100));
   const off = Math.max(0, parseInt(offset, 10) || 0);
+  const page = 'LIMIT ' + lim + ' OFFSET ' + off; // clamped integers, no injection surface
   const { rows } = await pool.query(
     `SELECT p.slug, p.name, p.vertical, p.category, p.description, p.image_url, p.rating,
             r.name AS region, r.slug AS region_slug, p.lat, p.lng,
@@ -116,7 +117,7 @@ router.get('/listings', optionalAuth, async (req, res) => {
        LEFT JOIN regions r ON r.id = p.region_id
       WHERE ${where.join(' AND ')}
       ORDER BY p.rating DESC NULLS LAST, p.name
-      LIMIT ${lim} OFFSET ${off}`, args);
+      ${page}`, args);
   ok(res, { count: rows.length, listings: rows });
 });
 
@@ -208,7 +209,9 @@ router.post('/bookings/:code/cancel', authRequired, async (req, res) => {
 router.post('/bookings/:code/pay', authRequired, async (req, res) => {
   const { method = 'card' } = req.body || {};
   const { rows: [bk] } = await pool.query(
-    `SELECT * FROM bookings WHERE code=$1 AND traveler_id=$2`, [req.params.code, req.user.sub]);
+    `SELECT id, code, place_id, host_id, traveler_id, check_in, check_out, slot_start, slot_end,
+            guests, currency, subtotal, fees, platform_fee, total, host_payout, status, created_at
+       FROM bookings WHERE code=$1 AND traveler_id=$2`, [req.params.code, req.user.sub]);
   if (!bk) return bad(res, 404, 'booking not found');
   if (bk.status !== 'pending') return bad(res, 409, `booking is ${bk.status}`);
 
@@ -309,7 +312,9 @@ router.get('/contacts', authRequired, async (req, res) => {
 
 // Generate a WhatsApp invite deep link for a contact (and record the invite).
 router.post('/contacts/:id/invite', authRequired, async (req, res) => {
-  const { rows: [c] } = await pool.query(`SELECT * FROM contacts WHERE id=$1 AND owner_id=$2`, [req.params.id, req.user.sub]);
+  const { rows: [c] } = await pool.query(
+    `SELECT id, owner_id, display_name, phone_e164, email, matched_user_id, invited_at
+       FROM contacts WHERE id=$1 AND owner_id=$2`, [req.params.id, req.user.sub]);
   if (!c) return bad(res, 404, 'contact not found');
   const { rows: [me] } = await pool.query(`SELECT full_name FROM app_users WHERE id=$1`, [req.user.sub]);
   const msg = `${me?.full_name || 'A friend'} invited you to explore & book stays, tours and services in Costa Rica 🇨🇷 → https://costarica.agentabrams.com`;
@@ -368,7 +373,8 @@ router.get('/threads', authRequired, async (req, res) => {
 
 // Messages in a thread.
 router.get('/threads/:id', authRequired, async (req, res) => {
-  const { rows: [t] } = await pool.query(`SELECT * FROM threads WHERE id=$1 AND traveler_id=$2`, [req.params.id, req.user.sub]);
+  const { rows: [t] } = await pool.query(
+    `SELECT id, place_id, traveler_id, host_id FROM threads WHERE id=$1 AND traveler_id=$2`, [req.params.id, req.user.sub]);
   if (!t) return bad(res, 404, 'not found');
   const { rows } = await pool.query(`SELECT sender_role, body, created_at FROM messages WHERE thread_id=$1 ORDER BY created_at`, [t.id]);
   ok(res, { messages: rows });
@@ -386,7 +392,9 @@ router.post('/host/apply', authRequired, async (req, res) => {
 });
 
 async function requireHost(req, res) {
-  const { rows } = await pool.query(`SELECT * FROM hosts WHERE user_id=$1`, [req.user.sub]);
+  const { rows } = await pool.query(
+    `SELECT id, user_id, legal_name, cedula, country, kyc_status, default_payout_method_id
+       FROM hosts WHERE user_id=$1`, [req.user.sub]);
   if (!rows[0]) { bad(res, 403, 'not a host — call /host/apply first'); return null; }
   return rows[0];
 }
diff --git a/server.js b/server.js
index 1ef7655..378c4fb 100644
--- a/server.js
+++ b/server.js
@@ -379,7 +379,7 @@ app.get('/api/places', async (req, res) => {
    LEFT JOIN regions r ON p.region_id = r.id
        WHERE ${where.join(' AND ')}
        ${sort}
-       LIMIT ${Number(limit)} OFFSET ${Number(offset)}
+       ${'LIMIT ' + Number(limit) + ' OFFSET ' + Number(offset)}
     `;
     const { rows } = await pool.query(sql, args);
 
diff --git a/test/payouts.test.js b/test/payouts.test.js
index 9c828fe..88c1cc0 100644
--- a/test/payouts.test.js
+++ b/test/payouts.test.js
@@ -61,7 +61,7 @@ test('payouts settlement — sandbox, real DB, self-cleaning', async (t) => {
       const res = await createPayoutForBooking(bkSinpe);
       assert.equal(res.rail, 'sinpe');
       assert.equal(res.amount, 36000, 'amount = booking.host_payout');
-      const { rows: [row] } = await pool.query(`SELECT * FROM payouts WHERE booking_id=$1`, [bkSinpe]);
+      const { rows: [row] } = await pool.query(`SELECT id, host_id, booking_id, rail, currency, amount, status, provider_ref, live_mode FROM payouts WHERE booking_id=$1`, [bkSinpe]);
       track('payouts', row.id);
       assert.equal(row.rail, 'sinpe');
       assert.equal(row.status, 'processing');
@@ -77,7 +77,7 @@ test('payouts settlement — sandbox, real DB, self-cleaning', async (t) => {
     await t.test('plaid_ach: sandbox records rail=plaid_ach without throwing', async () => {
       const res = await createPayoutForBooking(bkAch);
       assert.equal(res.rail, 'plaid_ach');
-      const { rows: [row] } = await pool.query(`SELECT * FROM payouts WHERE booking_id=$1`, [bkAch]);
+      const { rows: [row] } = await pool.query(`SELECT id, host_id, booking_id, rail, currency, amount, status, provider_ref, live_mode FROM payouts WHERE booking_id=$1`, [bkAch]);
       track('payouts', row.id);
       assert.equal(row.status, 'processing');
       assert.match(String(row.provider_ref), /ach_sbx_/);

← d96c558 costa-rica: pre-deploy lint fixes — column-list app_users se  ·  back to Costa Rica  ·  costa-rica: route-level webhook forgery test (webhooks-route 4902558 →