← back to Rentv Adintel

server.js

71 lines

'use strict';
/**
 * RENTV Advertiser Intelligence Viewer — Express entry point (house stack).
 * Mounts page routes + /api/v1. Simple View is the default executive UX (§25).
 * One-line module mount pattern (matches the DW/rentv fleet convention).
 */
require('./lib/env'); // loads .env into process.env (no dependency)

const path = require('path');
const express = require('express');

const app = express();
app.disable('x-powered-by');
app.use(express.json({ limit: '5mb' }));
app.use(express.urlencoded({ extended: true, limit: '5mb' }));

// Secure headers + a conservative CSP (§32). Vanilla pages only, no CDNs.
app.use((req, res, next) => {
  res.set('X-Content-Type-Options', 'nosniff');
  res.set('X-Frame-Options', 'SAMEORIGIN');
  res.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  res.set(
    'Content-Security-Policy',
    "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; base-uri 'self'; form-action 'self'"
  );
  next();
});

// Static assets (thumbnails, seed flyers, css, js).
app.use('/assets', express.static(path.join(__dirname, 'data', 'assets')));
app.use('/seed', express.static(path.join(__dirname, 'public', 'seed')));
app.use('/css', express.static(path.join(__dirname, 'public', 'css')));
app.use('/js', express.static(path.join(__dirname, 'public', 'js')));

app.get('/healthz', (req, res) => res.json({ ok: true, service: 'rentv-adintel', ts: Date.now() }));

// ---- Route modules (each guards its own existence so the app boots even
// ---- before a parallel worker has landed its file — graceful degradation). ----
function mountOptional(mountPath, modulePath) {
  try {
    const router = require(modulePath);
    app.use(mountPath, router);
    // eslint-disable-next-line no-console
    console.log(`[mount] ${mountPath} -> ${modulePath}`);
  } catch (e) {
    if (e.code === 'MODULE_NOT_FOUND' && e.message.includes(modulePath.replace('./', ''))) {
      console.warn(`[mount] SKIP ${mountPath} (${modulePath} not present yet)`);
    } else {
      throw e; // a real error inside the module — surface it
    }
  }
}

mountOptional('/api/v1', './src/routes/api');
mountOptional('/', './src/routes/pages');

// Fallback landing so a fresh clone shows something before pages/ lands.
app.get('/', (req, res, next) => {
  if (res.headersSent) return next();
  res
    .type('html')
    .send('<h1>RENTV Advertiser Intelligence</h1><p>App is booting. Run <code>npm run db:migrate && npm run db:seed</code>, then open <a href="/advertisers">/advertisers</a>.</p>');
});

const PORT = process.env.PORT || 9814;
if (require.main === module) {
  app.listen(PORT, () => console.log(`[rentv-adintel] listening on http://localhost:${PORT}`));
}

module.exports = app;