← back to Instant Audit

server.js

199 lines

// Instant Website Audit — productized SDCC-generator report as a sellable
// digital product. TEST MODE ONLY. Runs locally; never deploys itself.
//
// HARD RAILS (inherited from AbramsEgo money-side pattern):
//   - Stripe TEST keys only. sk_live_ is REFUSED at boot (money routes 503).
//     A live key means Steve is flipping the switch himself — not us.
//   - Absent key → checkout runs in MOCK mode: no real Stripe call, but the
//     full fulfillment pipeline still fires so the flow is demoable end-to-end.
//   - No real charges, no public exposure, no auto-send of the delivery email.
//
import 'dotenv/config';
import express from 'express';
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { fulfillOrder, TIER_SECTIONS } from './src/fulfill.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PORT = parseInt(process.env.PORT || '9982', 10);
const DATA_DIR = path.join(__dirname, 'data');
const REPORTS_DIR = path.join(__dirname, 'reports');
fs.mkdirSync(DATA_DIR, { recursive: true });
fs.mkdirSync(REPORTS_DIR, { recursive: true });
const ORDERS_LOG = path.join(DATA_DIR, 'orders.jsonl');

// ---- Stripe TEST-mode rail (mirrors AbramsEgo server.js) -------------------
const STRIPE_KEY = (process.env.STRIPE_SECRET_KEY || '').trim();
const STRIPE_WEBHOOK_SECRET = (process.env.STRIPE_WEBHOOK_SECRET || '').trim();
let stripe = null;
let stripeStatus = { configured: false, mode: 'mock', note: 'no test keys — MOCK checkout (fulfillment still runs)' };
if (STRIPE_KEY.startsWith('sk_live_')) {
  console.error('!!! STRIPE_SECRET_KEY is a LIVE key (sk_live_). Instant Website Audit is TEST-mode only — REFUSING to boot money routes. Going live is Steve-gated; remove the live key.');
  stripeStatus = { configured: false, mode: 'live-key-refused', note: 'LIVE key detected — money routes disabled (TEST mode only)' };
} else if (STRIPE_KEY.startsWith('sk_test_')) {
  try {
    const Stripe = (await import('stripe')).default;
    stripe = new Stripe(STRIPE_KEY);
    stripeStatus = { configured: true, mode: 'test', note: 'Stripe TEST mode — no real charges' };
  } catch (e) { console.error('stripe init failed:', e.message); }
} else if (STRIPE_KEY) {
  console.error('STRIPE_SECRET_KEY set but not sk_test_ — ignoring (TEST keys only).');
}

const TIERS = {
  lite:     { label: 'Audit Lite',     usd: 49,  blurb: 'Full site audit + a pack of working visitor tools.' },
  standard: { label: 'Audit Standard', usd: 99,  blurb: 'Everything in Lite + peer survey + trusted-sources hub.' },
  pro:      { label: 'Audit Pro',      usd: 199, blurb: 'Everything + competitor teardown + hand-built homepage concepts.' },
};

const app = express();

// Webhook needs the RAW body — register BEFORE express.json().
app.post('/api/stripe/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  if (!stripe || !STRIPE_WEBHOOK_SECRET) return res.status(503).json({ error: 'stripe not configured' });
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], STRIPE_WEBHOOK_SECRET);
  } catch (e) { return res.status(400).json({ error: `signature verification failed: ${e.message}` }); }
  if (event.type === 'checkout.session.completed') {
    const s = event.data.object;
    const m = s.metadata || {};
    await runFulfillment({ orderId: m.orderId || s.id, url: m.url, email: s.customer_email || m.email, tier: m.tier, baseUrl: baseUrlFrom(req) });
  }
  res.json({ received: true });
});

app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));

function baseUrlFrom(req) { return `${req.protocol}://${req.get('host')}`; }

// ---- sell config for the landing CTAs -------------------------------------
app.get('/api/sell/config', (req, res) => res.json({
  mode: stripeStatus.mode,
  checkoutReady: true, // mock mode is always "ready" locally
  note: stripeStatus.note,
  tiers: Object.entries(TIERS).map(([k, v]) => ({ key: k, ...v })),
}));

// ---- checkout: real Stripe TEST session, or MOCK when no keys --------------
app.post('/api/checkout', async (req, res) => {
  const { tier, url, email } = req.body || {};
  const t = TIERS[tier];
  if (!t) return res.status(400).json({ error: `tier must be one of ${Object.keys(TIERS).join(', ')}` });
  if (!url || !/^https?:\/\//i.test(url)) return res.status(400).json({ error: 'a valid http(s) website URL is required' });
  const orderId = 'ord_' + crypto.randomBytes(6).toString('hex');
  const base = baseUrlFrom(req);

  if (stripe) {
    // Real Stripe TEST-mode Checkout Session (one-time payment).
    try {
      const session = await stripe.checkout.sessions.create({
        mode: 'payment',
        customer_email: email || undefined,
        line_items: [{
          quantity: 1,
          price_data: {
            currency: 'usd',
            unit_amount: t.usd * 100,
            product_data: { name: `${t.label} — website audit`, description: t.blurb },
          },
        }],
        metadata: { orderId, tier, url, email: email || '' },
        success_url: `${base}/success?order=${orderId}&session_id={CHECKOUT_SESSION_ID}`,
        cancel_url: `${base}/?checkout=cancel`,
      });
      logOrder({ orderId, tier, url, email, mode: 'test', status: 'checkout_created', session: session.id });
      return res.json({ ok: true, mode: 'test', url: session.url, orderId });
    } catch (e) {
      return res.status(502).json({ error: `stripe error: ${e.message}` });
    }
  }

  // MOCK mode: no keys installed. Simulate a paid order and go straight to the
  // success flow so the full pipeline is demoable. NO money moves.
  logOrder({ orderId, tier, url, email, mode: 'mock', status: 'mock_paid' });
  return res.json({ ok: true, mode: 'mock', url: `${base}/success?order=${orderId}&mock=1`, orderId });
});

// ---- success page: triggers fulfillment, shows delivery link ---------------
app.get('/success', async (req, res) => {
  const orderId = String(req.query.order || '');
  const mock = req.query.mock === '1';
  const order = findOrder(orderId);
  if (!order) return res.status(404).send(page('Order not found', '<p>We could not find that order.</p>'));

  // In real Stripe mode fulfillment is driven by the webhook. But for a smooth
  // local demo (and mock mode) we also fulfill here idempotently if not done.
  let result = readManifest(orderId);
  if (!result) result = await runFulfillment({ ...order, baseUrl: baseUrlFrom(req) });

  const deliveryUrl = `/r/${orderId}/`;
  const modeBadge = mock || order.mode === 'mock'
    ? '<span style="background:#fef3c7;border:1px solid #e4ddd2;padding:3px 8px;border-radius:3px;font-size:12px">MOCK ORDER · no charge</span>'
    : '<span style="background:#e6f4ea;border:1px solid #cde;padding:3px 8px;border-radius:3px;font-size:12px">TEST payment · no real charge</span>';
  res.send(page(`Your audit is ready`, `
    ${modeBadge}
    <h1 style="margin-top:14px">Your ${TIERS[order.tier]?.label || order.tier} audit is ready</h1>
    <p class="host">${result.manifest.host} · ${result.manifest.sections.length} sections</p>
    <p><a class="cta" href="${deliveryUrl}">Open your report →</a></p>
    <p style="font-size:14px;color:#6b6257">A delivery email has been <strong>drafted</strong> (not sent — sending is gated).
    You can preview it <a href="/r/${orderId}/delivery-email.txt">here</a>.</p>
  `));
});

// ---- serve the generated report bundles ------------------------------------
app.use('/r', express.static(REPORTS_DIR));

// ---- admin: list orders (local only) ---------------------------------------
app.get('/api/orders', (req, res) => res.json(readOrders()));

// ---- fulfillment runner (idempotent) ---------------------------------------
async function runFulfillment(order) {
  const { orderId, url, email, tier, baseUrl } = order;
  const existing = readManifest(orderId);
  if (existing) return { manifest: existing };
  const result = await fulfillOrder({ orderId, url, email, tier, baseUrl });
  logOrder({ orderId, tier, url, email, status: 'fulfilled' });
  console.log(`[fulfill] ${orderId} → ${result.deliveryUrl} (${result.files.length} files) $0 (local)`);
  return result;
}

// ---- tiny order log helpers ------------------------------------------------
function logOrder(entry) {
  fs.appendFileSync(ORDERS_LOG, JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n');
}
function readOrders() {
  if (!fs.existsSync(ORDERS_LOG)) return [];
  return fs.readFileSync(ORDERS_LOG, 'utf8').trim().split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
}
function findOrder(orderId) {
  // Reconstruct the freshest view of an order from the log.
  const rows = readOrders().filter((r) => r.orderId === orderId);
  if (!rows.length) return null;
  return Object.assign({}, ...rows);
}
function readManifest(orderId) {
  const f = path.join(REPORTS_DIR, orderId, 'manifest.json');
  if (!fs.existsSync(f)) return null;
  try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; }
}

function page(title, body) {
  return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${title}</title><style>
body{font-family:-apple-system,Segoe UI,sans-serif;background:#faf8f4;color:#1a1a1a;line-height:1.6;max-width:640px;margin:0 auto;padding:60px 24px}
h1{font-family:Georgia,serif;font-size:30px}
.host{color:#6b6257}
.cta{display:inline-block;background:#1a1a1a;color:#fff;text-decoration:none;padding:12px 22px;border-radius:4px;font-size:15px;margin:10px 0}
.cta:hover{background:#8a7355}
a{color:#8a7355}
</style></head><body>${body}</body></html>`;
}

app.listen(PORT, '127.0.0.1', () => {
  console.log(`Instant Website Audit on http://127.0.0.1:${PORT}  [stripe: ${stripeStatus.mode}]`);
});