← 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
M lib/payouts.jsM routes/app.jsM server.jsM test/payouts.test.js
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 →