← back to Costa Rica
routes/admin.js
67 lines
'use strict';
// Admin API — sits BEHIND the site basic-auth gate (mounted after it in server.js).
// Host-claim approvals + bookings oversight. Every record carries created_at so
// the admin UI can show date+time on each card (standing rule).
const express = require('express');
const { pool } = require('../lib/db');
const router = express.Router();
router.get('/stats', async (_req, res) => {
const { rows: [s] } = await pool.query(`
SELECT (SELECT count(*) FROM bookings) bookings,
(SELECT count(*) FROM bookings WHERE status='confirmed') confirmed,
(SELECT COALESCE(SUM(total),0) FROM bookings WHERE status IN ('confirmed','completed')) gmv_minor,
(SELECT COALESCE(SUM(platform_fee),0) FROM bookings WHERE status IN ('confirmed','completed')) revenue_minor,
(SELECT count(*) FROM place_hosts WHERE claim_status='pending') pending_claims,
(SELECT count(*) FROM place_booking WHERE is_active) bookable,
(SELECT count(*) FROM hosts) hosts`);
res.json({ ok: true, stats: s });
});
router.get('/claims', async (req, res) => {
const status = req.query.status || 'pending';
const { rows } = await pool.query(`
SELECT ph.place_id, ph.host_id, ph.claim_status, ph.created_at,
p.name AS place_name, p.slug AS place_slug,
h.legal_name, h.cedula, h.country, u.email
FROM place_hosts ph
JOIN places p ON p.id=ph.place_id
JOIN hosts h ON h.id=ph.host_id
JOIN app_users u ON u.id=h.user_id
WHERE ($1='all' OR ph.claim_status=$1)
ORDER BY ph.created_at DESC LIMIT 200`, [status]);
res.json({ ok: true, claims: rows });
});
router.post('/claims/:placeId/:hostId', async (req, res) => {
// CSRF guard (Cody admin audit, cycle 28): basic-auth creds are auto-attached by
// the browser cross-site, and express.urlencoded is mounted globally, so a
// cross-site <form> POST (application/x-www-form-urlencoded — a "simple" request,
// no CORS preflight) could drive this state mutation against a logged-in admin.
// Require application/json: a cross-site simple form cannot set it without
// triggering a preflight that admin CORS does not allow, so the forged POST is
// rejected; the real admin UI already sends JSON (public/admin.html).
if (!req.is('application/json')) return res.status(415).json({ ok: false, error: 'admin mutations require application/json' });
const { decision } = req.body || {};
if (!['approved', 'rejected'].includes(decision)) return res.status(400).json({ ok: false, error: 'bad decision' });
const { rows } = await pool.query(
`UPDATE place_hosts SET claim_status=$1 WHERE place_id=$2 AND host_id=$3 RETURNING *`,
[decision, req.params.placeId, req.params.hostId]);
if (!rows[0]) return res.status(404).json({ ok: false, error: 'claim not found' });
res.json({ ok: true, claim: rows[0] });
});
router.get('/bookings', async (req, res) => {
const { rows } = await pool.query(`
SELECT b.code, b.status, b.currency, b.total, b.host_payout, b.platform_fee,
b.check_in, b.check_out, b.guests, b.created_at,
p.name AS place_name, u.email AS traveler_email
FROM bookings b
JOIN places p ON p.id=b.place_id
JOIN app_users u ON u.id=b.traveler_id
ORDER BY b.created_at DESC LIMIT 200`);
res.json({ ok: true, bookings: rows });
});
module.exports = router;