← back to Greenland Onboard

server.js

146 lines

// greenland.internal.designerwallcoverings.com — INTERNAL employee cork-line tool.
// Basic-auth gated (admin/DW2024!). Look up a Cork SKU → specs, stock, pricing (net cost /
// retail / discount / margin — INTERNAL ONLY, never customer-facing). Private-label brand is
// Phillipe Romano; "Greenland" appears only here (auth-gated internal tool), never customer-facing.
//
// Zero-dependency (built-in http). Serves public/ (viewer.json snapshot + LOCAL images only —
// NEVER hotlinks a CDN). Live stock re-check hits the open Greenland JSON API ($0, no auth).
//
// Env: PORT (default 9973) · BASIC_AUTH="user:pass" (default admin:DW2024!)
//      GL_API (default https://api.greenlandwallcoverings.com/api)
const http = require('http');
const https = require('https');
const zlib = require('zlib');
const fs = require('fs');
const path = require('path');

const PORT = process.env.PORT || 9973;
const ROOT = path.join(__dirname, 'public');
const SNAP = path.join(ROOT, 'viewer.json');
const GL_API = process.env.GL_API || 'https://api.greenlandwallcoverings.com/api';
const [AUTH_USER, AUTH_PASS] = (process.env.BASIC_AUTH || 'admin:DW2024!').split(':');

const TYPES = {
  '.html': 'text/html;charset=utf-8', '.js': 'application/javascript', '.css': 'text/css',
  '.json': 'application/json', '.svg': 'image/svg+xml', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
  '.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp', '.avif': 'image/avif',
};

// ── in-memory snapshot (hot-reloaded when build-viewer-snapshot.mjs rewrites it) ──
let DATA = { products: [], count: 0 };
let GL_BY_ID = new Map(); // gl_id -> product (for live-stock lookup)
function loadSnapshot() {
  try {
    DATA = JSON.parse(fs.readFileSync(SNAP, 'utf8'));
    GL_BY_ID = new Map(DATA.products.filter(p => p.gl_id != null).map(p => [String(p.gl_id), p]));
    console.log(`[greenland-internal] loaded ${DATA.count} cork SKUs (generated ${DATA.generated_at})`);
  } catch (e) { console.error('[greenland-internal] snapshot load failed:', e.message); }
}
loadSnapshot();
fs.watchFile(SNAP, { interval: 5000 }, (c, p) => { if (c.mtimeMs !== p.mtimeMs) loadSnapshot(); });

function send(res, code, body, headers = {}) {
  res.writeHead(code, { 'cache-control': 'no-cache', ...headers });
  res.end(body);
}
function sendJson(req, res, obj, code = 200) {
  const buf = Buffer.from(JSON.stringify(obj));
  const enc = (req.headers['accept-encoding'] || '').includes('gzip');
  const h = { 'content-type': 'application/json', 'cache-control': 'no-cache' };
  if (enc && buf.length > 512) { h['content-encoding'] = 'gzip'; return send(res, code, zlib.gzipSync(buf), h); }
  send(res, code, buf, h);
}

// ── live stock re-check: open Greenland API GET /api/product/<gl_id> ($0, no auth) ──
function glProductLive(glId) {
  return new Promise((resolve, reject) => {
    const url = `${GL_API}/product/${encodeURIComponent(glId)}`;
    const req = https.get(url, { timeout: 15000 }, r => {
      let s = '';
      r.on('data', d => (s += d));
      r.on('end', () => { try { const j = JSON.parse(s); resolve(j.data || j); } catch (e) { reject(e); } });
    });
    req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
    req.on('error', reject);
  });
}

// Internal purchasing requests (Memo/Stock/Price) — reuse the shared express module
// on a tiny sub-app; the raw-http router below delegates /api/request* to it.
const express = require('express');
const REQ_APP = express();
REQ_APP.use(express.json());
require('./lib/vendor-requests').mountVendorRequests(REQ_APP, {
  vendorCode: 'greenland',
  getVendor: () => ({ name: 'Greenland' }),
  dataDir: __dirname + '/data',
});

const server = http.createServer(async (req, res) => {
  const u = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
  const p = decodeURIComponent(u.pathname);

  // Unauthenticated health probe (deploy smoke test) — BEFORE the auth gate.
  if (p === '/healthz') return sendJson(req, res, { ok: true, products: DATA.count, generated_at: DATA.generated_at });

  // ── HTTP Basic Auth — internal-only gate ──
  const hdr = req.headers.authorization || '';
  const [scheme, b64] = hdr.split(' ');
  let ok = false;
  if (scheme === 'Basic' && b64) {
    const [uu, pp] = Buffer.from(b64, 'base64').toString('utf8').split(':');
    ok = uu === AUTH_USER && pp === AUTH_PASS;
  }
  if (!ok) {
    res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Phillipe Romano Cork - internal"' });
    return res.end('Authentication required.');
  }

  // ── Internal purchasing requests → shared vendor_requests module ──
  if (p === '/api/request' || p === '/api/requests' || p === '/api/request/preview') return REQ_APP(req, res);

  // ── API: full snapshot (grid + facets + pricing constants) ──
  if (p === '/api/products') return sendJson(req, res, DATA);

  // ── API: one product by DW SKU ──
  if (p.startsWith('/api/product/')) {
    const sku = p.slice('/api/product/'.length);
    const prod = DATA.products.find(x => x.dw_sku === sku);
    return prod ? sendJson(req, res, prod) : sendJson(req, res, { error: 'not found' }, 404);
  }

  // ── API: live stock re-check (metered? NO — $0 open API) for one DW SKU ──
  if (p.startsWith('/api/stock/')) {
    const sku = p.slice('/api/stock/'.length);
    const prod = DATA.products.find(x => x.dw_sku === sku);
    if (!prod) return sendJson(req, res, { error: 'not found' }, 404);
    const pid = prod.gl_product_id != null ? prod.gl_product_id : prod.gl_id;
    if (pid == null) return sendJson(req, res, { dw_sku: sku, live: false, error: 'no product id' }, 200);
    try {
      const live = await glProductLive(pid);
      const published = live.isPublish === 1 || live.isPublish === true;
      const status = live.status;
      const skuCount = Array.isArray(live.skus) ? live.skus.length : null;
      return sendJson(req, res, {
        dw_sku: sku, gl_product_id: pid, live: true, checked_at: new Date().toISOString(),
        in_catalog: published, status,
        availability: published ? 'available' : 'unlisted',
        colorways_live: skuCount,
        cost_note: 'Greenland API exposes no per-SKU inventory count; availability = published-in-catalog signal.',
      });
    } catch (e) {
      return sendJson(req, res, { dw_sku: sku, live: false, error: e.message }, 502);
    }
  }

  // ── static files (viewer.json, LOCAL images, index.html) ──
  let fp = p === '/' ? '/index.html' : p;
  fp = path.join(ROOT, path.normalize(fp).replace(/^(\.\.[/\\])+/, ''));
  fs.readFile(fp, (err, buf) => {
    if (err) return send(res, 404, 'Not found', { 'content-type': 'text/plain' });
    send(res, 200, buf, { 'content-type': TYPES[path.extname(fp)] || 'application/octet-stream' });
  });
});

server.listen(PORT, () => console.log(`Greenland Cork internal tool → http://127.0.0.1:${PORT} (auth: ${AUTH_USER})`));