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