← back to Flockedwallpaper

server.js

152 lines

const http = require('http');
const fs = require('fs');
const path = require('path');
require('dotenv').config({ path: process.env.DOTENV_PATH || '/root/DW-Agents/claudette-agent/.env' });
const { Pool } = require('pg');
const apiVendorRedact = require('../_shared/api-vendor-redact'); // default export = Express-style middleware; also handles GET /img/<token> image proxy
const { redact } = apiVendorRedact; // scrub DW vendor names from /api responses (standing rule; raw-http server, so applied by hand)

const PORT = process.env.PORT || 3200;
const pool = new Pool({ connectionString: process.env.DB_URL || process.env.DATABASE_URL });

// Pre-warm the /img token map at boot so /img/<token> resolves immediately after a
// pm2 restart (closes the cold-start 404 window). warm(products) registers token->url
// for every product image url. Safe no-op if the catalog can't load.
async function warmImgMap() {
    if (typeof apiVendorRedact.warm !== 'function') return;
    try { apiVendorRedact.warm(await getProducts()); } catch (e) { /* cold map; lazily filled by /api/products */ }
}

// Serve GET /img/<token> by delegating to the redact module's image proxy. The module's
// default export is Express-style middleware that handles /img/ when req.path starts with
// it; we adapt the raw-http req/res to the minimal shape it needs (req.path, res.status,
// res.setHeader, res.end). On an unknown token the middleware calls next() -> 404 here.
function serveImg(req, requestPath, res) {
    const shimReq = { method: 'GET', path: requestPath };
    res.status = (code) => { res.statusCode = code; return res; };
    const next = () => { res.writeHead(404, { 'Content-Type': 'text/html' }); res.end('<h1>404 - File Not Found</h1>', 'utf-8'); };
    return apiVendorRedact(shimReq, res, next);
}

let productsCache = null;
let productsCacheAt = 0;
async function getProducts() {
    const now = Date.now();
    if (productsCache && now - productsCacheAt < 60_000) return productsCache;
    const { rows } = await pool.query(`
        SELECT id, title, handle, description, vendor, product_type AS "productType",
               tags, primary_color AS "primaryColor", skus, primary_sku AS "primarySku",
               price, image, image_alt AS "imageAlt", product_url AS "productUrl",
               status, created_at AS "createdAt"
        FROM flocked_products
        WHERE image IS NOT NULL
        ORDER BY title
    `);
    // DW vendor-name redaction: never expose real supplier names in customer-facing API.
    for (const r of rows) r.vendor = 'Designer Wallcoverings';
    productsCache = rows;
    productsCacheAt = now;
    return rows;
}

const mimeTypes = {
    '.html': 'text/html',
    '.css': 'text/css',
    '.js': 'text/javascript',
    '.json': 'application/json',
    '.png': 'image/png',
    '.jpg': 'image/jpeg',
    '.jpeg': 'image/jpeg',
    '.gif': 'image/gif',
    '.svg': 'image/svg+xml',
    '.ico': 'image/x-icon'
};

const server = http.createServer((req, res) => {
    // Add CORS headers for all requests
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

    // Prevent caching
    res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
    res.setHeader('Pragma', 'no-cache');
    res.setHeader('Expires', '0');

    // Vendor-neutral image proxy: /img/<token> streams the original CDN image without
    // exposing the supplier name in the url (the redact module rewrites image urls to
    // these tokens in /api/products). serveImg sets its own immutable Cache-Control.
    const imgPath = req.url.split('?')[0];
    if (req.method === 'GET' && imgPath.indexOf('/img/') === 0) {
        return serveImg(req, imgPath, res);
    }

    if (req.url === '/api/products' || req.url.startsWith('/api/products?')) {
        getProducts()
            .then(rows => {
                res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=60' });
                res.end(JSON.stringify(redact(rows)));
            })
            .catch(err => {
                console.error('/api/products error:', err.message);
                res.writeHead(500, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ error: 'db_error', message: err.message }));
            });
        return;
    }

    // Remove query string parameters from URL
    let requestPath = req.url.split('?')[0];
    try { requestPath = decodeURIComponent(requestPath); } catch (e) { requestPath = requestPath; }
    if (requestPath === '/') {
        requestPath = '/index.html';
    }

    // Confine all file serving to the project root — block path traversal
    const root = __dirname;
    const filePath = path.join(root, requestPath);
    if (filePath !== root && !filePath.startsWith(root + path.sep)) {
        res.writeHead(403, { 'Content-Type': 'text/html' });
        res.end('<h1>403 - Forbidden</h1>', 'utf-8');
        return;
    }

    // Never serve scratch / test / backup / server-source files even if present on disk
    // (index-old-backup.html, simple-test.html, diagnostic.html, server-old-backup.js, etc.).
    // Return 404 so their existence isn't revealed.
    const baseName = path.basename(filePath);
    if (/\.(bak|orig|save|old|swp)$|\.backup\.json$|\.pre-|-backup\.|-old\.|^test-|-test\.|^simple-test|^diagnostic|^test-load|^server(-|\.js$|-old)/i.test(baseName) ||
        /^package(-lock)?\.json$/i.test(baseName) ||
        /(^|-)products\.json$/i.test(baseName)) {   // also 404 raw catalog json (page uses /api/products)
        res.writeHead(404, { 'Content-Type': 'text/html' });
        res.end('<h1>404 - File Not Found</h1>', 'utf-8');
        return;
    }

    const extname = String(path.extname(filePath)).toLowerCase();
    const contentType = mimeTypes[extname] || 'application/octet-stream';

    fs.readFile(filePath, (error, content) => {
        if (error) {
            if (error.code === 'ENOENT') {
                res.writeHead(404, { 'Content-Type': 'text/html' });
                res.end('<h1>404 - File Not Found</h1>', 'utf-8');
            } else {
                res.writeHead(500);
                res.end(`Server Error: ${error.code}`, 'utf-8');
            }
        } else {
            res.writeHead(200, { 'Content-Type': contentType });
            res.end(content, 'utf-8');
        }
    });
});

server.listen(PORT, '0.0.0.0', () => {
    console.log(`FlockedWallpaper.com server running at:`);
    console.log(`  Local: http://localhost:${PORT}/`);
    console.log(`  External: http://45.61.58.125:${PORT}/`);
    console.log('Press Ctrl+C to stop the server');
    warmImgMap().then(() => console.log('  /img token map pre-warmed'));
});