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