← back to Glassbeadedwallpaper

server.js

194 lines

const http = require('http');
const fs = require('fs');
const path = require('path');
require('dotenv').config({ path: '/root/DW-Agents/claudette-agent/.env' });
const { Pool } = require('pg');
const { redact } = require('../_shared/api-vendor-redact'); // scrub DW vendor names from /api responses (standing rule; raw-http server, so applied by hand)

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

let productsCache = null;
let productsCacheAt = 0;
// Brand-scrub: PG data still has "Designer Wallcoverings" in vendor/imageAlt/description
// fields from the original Shopify import. Strip & rebrand to Maison Lustre on read.
const BRAND_NAME = 'Maison Lustre';
function brandScrub(text) {
  if (!text || typeof text !== 'string') return text;
  return text
    .replace(/\s*[-—|·]\s*Designer Wallcoverings(?:\s+and\s+Fabrics)?[^,]*$/g, '')
    .replace(/Available Exclusively at Designer Wallcoverings(?:\s+and\s+Fabrics)?/gi, `Available at ${BRAND_NAME}`)
    .replace(/Designer Wallcoverings(?:\s+and\s+Fabrics)?/g, BRAND_NAME);
}
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", category, skus, primary_sku AS "primarySku",
               price, image, image_alt AS "imageAlt", product_url AS "productUrl",
               status, fire_rated AS "fireRated", created_at AS "createdAt"
        FROM glass_bead_products
        WHERE image IS NOT NULL
        ORDER BY title
    `);
    // Apply brand-scrub to text fields
    for (const r of rows) {
      r.vendor = BRAND_NAME;  // full redaction: never expose real supplier names (brandScrub only caught the "Designer Wallcoverings" string, real vendors like "Maya Romanoff" leaked)
      r.imageAlt = brandScrub(r.imageAlt);
      r.description = brandScrub(r.description);
      r.title = brandScrub(r.title);
    }
    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',
    '.webp': 'image/webp'
};

// Health check endpoint for monitoring
function handleHealthCheck(res) {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
        status: 'healthy',
        service: 'glassbeadedwallpaper.com',
        port: PORT,
        timestamp: new Date().toISOString(),
        uptime: process.uptime()
    }));
}

const server = http.createServer((req, res) => {
    // Health check endpoint
    if (req.url === '/health' || req.url === '/health/') {
        return handleHealthCheck(res);
    }

    if (req.url === '/api/products' || req.url.startsWith('/api/products?')) {
        getProducts()
            .then(rows => {
                res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store, must-revalidate' });
                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;
    }

    // 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 for development/testing
    res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
    res.setHeader('Pragma', 'no-cache');
    res.setHeader('Expires', '0');

    // Remove query string parameters from URL
    let requestPath;
    try {
        requestPath = decodeURIComponent(req.url.split('?')[0]);
    } catch (e) {
        res.writeHead(400, { 'Content-Type': 'text/html' });
        res.end('<h1>400 - Bad Request</h1>', 'utf-8');
        return;
    }
    if (requestPath === '/') {
        requestPath = '/index.html';
    }

    // Resolve against the project root and reject any path that escapes it
    const rootDir = __dirname;
    const filePath = path.join(rootDir, requestPath);
    if (filePath !== rootDir && !filePath.startsWith(rootDir + path.sep)) {
        res.writeHead(403, { 'Content-Type': 'text/html' });
        res.end('<h1>403 - Forbidden</h1>', 'utf-8');
        return;
    }

    // Never serve backup / scratch / source-control files even if they exist on disk
    // (e.g. *.backup.json carries un-scrubbed internal vendor data). Return 404 so the
    // path's existence isn't revealed.
    const baseName = path.basename(filePath);
    if (/\.(bak|orig|save|old|swp)$|\.backup\.json$|\.pre-|-backup\.|-old\.|^\.env/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><p>The requested resource could not be found on this server.</p>', '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');
        }
    });
});

// Error handling
server.on('error', (error) => {
    if (error.code === 'EADDRINUSE') {
        console.error(`❌ Port ${PORT} is already in use. Please stop the existing process or use a different port.`);
        process.exit(1);
    } else {
        console.error('❌ Server error:', error);
    }
});

// Graceful shutdown
process.on('SIGTERM', () => {
    console.log('⚠️  SIGTERM signal received: closing HTTP server');
    server.close(() => {
        console.log('✓ HTTP server closed');
        process.exit(0);
    });
});

process.on('SIGINT', () => {
    console.log('\n⚠️  SIGINT signal received: closing HTTP server');
    server.close(() => {
        console.log('✓ HTTP server closed');
        process.exit(0);
    });
});

// Start server
server.listen(PORT, '0.0.0.0', () => {
    console.log('╔════════════════════════════════════════════════════════════╗');
    console.log('║   GlassBeadedWallpaper.com Server                         ║');
    console.log('╠════════════════════════════════════════════════════════════╣');
    console.log(`║   Status: RUNNING                                          ║`);
    console.log(`║   Port: ${PORT}                                               ║`);
    console.log(`║   Local: http://localhost:${PORT}/                           ║`);
    console.log(`║   Health: http://localhost:${PORT}/health                    ║`);
    console.log('║   Press Ctrl+C to stop                                     ║');
    console.log('╚════════════════════════════════════════════════════════════╝');
});