← back to Consulting Rentv Com

server.js

101 lines

'use strict';
/**
 * RENTV.com — Consulting portal.
 * Generated by the `consulting` skill (fuses fantasea-consulting + prestige-car-wash).
 *
 * Routes:
 *   GET  /              -> portal.html   (UNGATED at app; nginx gates domain) — LANDS on the deliverable
 *   GET  /welcome       -> signin.html   (UNGATED) — client info + credentials card
 *   GET  /intake        -> intake.html   (UNGATED) — new-client questionnaire
 *   POST /api/intake    -> append to data/intakes.json (UNGATED submit)
 *   GET  /api/health    -> {ok:true}     (UNGATED)
 *   GET  /portal        -> portal.html   (GATED) — the consulting deliverable
 *   GET  /admin         -> admin/index.html (GATED) — integrated-social command center
 *   GET  /api/admin/:bucket -> data/<bucket>.json (GATED read)
 *   POST /api/admin/:bucket -> overwrite data/<bucket>.json (GATED write)
 */
const fs = require('fs');
const path = require('path');
const express = require('express');
try { require('dotenv').config(); } catch { /* dotenv optional */ }

const app = express();
app.use(express.json({ limit: '1mb' }));
const PORT = process.env.PORT || 9701;
const PUB = path.join(__dirname, 'public');
const DATA = path.join(__dirname, 'data');

// ---- credentials -----------------------------------------------------------
// Unified fleet login + this client's own login. Extra pairs via BASIC_AUTH_EXTRA
// (comma-separated user:pass). These same creds are shown on the sign-in page.
const CREDS = ['admin:DW2024!', 'client:Rentvc2026!', 'Boomer:rentfree911', 'Daniel:Dandeana1992', 'Scott:Russia1980']
  .concat((process.env.BASIC_AUTH_EXTRA || '').split(',').map((s) => s.trim()).filter(Boolean));
const ACCEPTED = new Set(CREDS.map((c) => 'Basic ' + Buffer.from(c).toString('base64')));
function gate(req, res, next) {
  if (ACCEPTED.has(req.headers.authorization || '')) return next();
  res.set('WWW-Authenticate', 'Basic realm="RENTV.com Portal"');
  return res.status(401).send('Authentication required');
}

// ---- data helpers ----------------------------------------------------------
const readJSON = (f, fb) => { try { return JSON.parse(fs.readFileSync(path.join(DATA, f), 'utf8')); } catch { return fb; } };
const writeJSON = (f, v) => fs.writeFileSync(path.join(DATA, f), JSON.stringify(v, null, 2));
const BUCKETS = ['client', 'socials', 'suggestions', 'competitors', 'best-times', 'content-calendar', 'ads', 'directories', 'media', 'intakes', 'crm'];

// ---- ungated routes --------------------------------------------------------
// Root LANDS on the actual deliverable (Steve 2026-07-20: "land, not just
// forwarded"). The whole domain is already behind one nginx basic-auth, so the
// portal opens directly — no click-through sign-in card. The branded
// credentials/welcome card is preserved at /welcome for reference.
app.get('/', (_req, res) => res.sendFile(path.join(PUB, 'portal.html')));
app.get('/welcome', (_req, res) => res.sendFile(path.join(PUB, 'signin.html')));
app.get('/intake', (_req, res) => res.sendFile(path.join(PUB, 'intake.html')));
app.get('/api/health', (_req, res) => res.json({ ok: true, client: 'RENTV.com', at: new Date().toISOString() }));
app.get('/api/client', (_req, res) => res.json(readJSON('client.json', {}))); // public-safe summary for signin page
app.post('/api/intake', (req, res) => {
  const body = req.body || {};
  const at = new Date().toISOString();
  const all = readJSON('intakes.json', []);
  all.push({ received_at: at, source: 'form', intake: body });
  writeJSON('intakes.json', all);
  // Fan the client's chosen command-center modules into the CRM bucket as
  // tracked items so the admin command center picks them up immediately.
  const mods = Array.isArray(body.crm_modules) ? body.crm_modules : [];
  if (mods.length) {
    const crm = readJSON('crm.json', []);
    mods.forEach((m) => crm.push({
      created_at: at,
      module_id: m && m.id, module: (m && m.label) || (m && m.id),
      business: body.business_name || null,
      contact: body.contact_name || null,
      email: body.contact_email || null,
      status: 'assigned', source: 'intake',
    }));
    writeJSON('crm.json', crm);
  }
  res.json({ ok: true, message: 'Thanks — your consulting intake was received.' });
});

// ---- gated routes ----------------------------------------------------------
app.get('/portal', gate, (_req, res) => res.sendFile(path.join(PUB, 'portal.html')));
app.get('/admin', gate, (_req, res) => res.sendFile(path.join(PUB, 'admin', 'index.html')));
app.get('/slideshow', gate, (_req, res) => res.sendFile(path.join(PUB, 'slideshow.html'))); // deck view of the deliverable
app.get('/api/admin/:bucket', gate, (req, res) => {
  if (!BUCKETS.includes(req.params.bucket)) return res.status(404).json({ error: 'unknown bucket' });
  res.json(readJSON(req.params.bucket + '.json', []));
});
app.post('/api/admin/:bucket', gate, (req, res) => {
  if (!BUCKETS.includes(req.params.bucket)) return res.status(404).json({ error: 'unknown bucket' });
  writeJSON(req.params.bucket + '.json', req.body);
  res.json({ ok: true });
});

// gated static (portal assets + generated version concepts)
app.use('/portal', gate, express.static(path.join(PUB), { extensions: ['html'] }));
app.use('/versions', gate, express.static(path.join(PUB, 'versions'), { extensions: ['html'] }));
app.use('/admin', gate, express.static(path.join(PUB, 'admin'), { extensions: ['html'] }));
// ungated static (only the two entry pages + shared img)
app.use('/img', express.static(path.join(PUB, 'img')));

app.listen(PORT, () => console.log('RENTV.com consulting portal on ' + PORT));