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