← back to Dw Showroom

server.js

210 lines

const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const path = require('path');
const { Pool } = require('pg');

const app = express();
// Security headers via helmet (added 2026-05-04 overnight YOLO loop)
app.use(helmet({ contentSecurityPolicy: false }));
const PORT = process.env.PORT || 7685;
const AUTH_USER = 'admin';
const AUTH_PASS = 'DWSecure2024!';

// Database
const pool = new Pool({
  connectionString: 'postgresql://dw_admin@127.0.0.1:5432/dw_unified',
  max: 5,
  idleTimeoutMillis: 10000,
  connectionTimeoutMillis: 5000,
  statement_timeout: 10000
});

// Middleware
app.use(compression());
app.use(express.json());

// Basic auth
app.use((req, res, next) => {
  if (req.path.startsWith('/api/') || req.path === '/' || req.path.startsWith('/js/') || req.path.startsWith('/css/') || req.path.startsWith('/photos/') || req.path.startsWith('/textures/')) {
    const auth = req.headers.authorization;
    if (!auth || !auth.startsWith('Basic ')) {
      res.setHeader('WWW-Authenticate', 'Basic realm="DW Showroom"');
      return res.status(401).send('Authentication required');
    }
    const [user, pass] = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':');
    if (user !== AUTH_USER || pass !== AUTH_PASS) {
      return res.status(403).send('Forbidden');
    }
  }
  next();
});

// Static files
app.use(express.static(path.join(__dirname, 'public')));
app.use('/photos', express.static(path.join(__dirname, 'photos/jpg')));

// Routes: Sherman Oaks 3D showroom
app.get('/sherman-oaks', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'sherman-oaks.html'));
});

// Health check (no DB)
app.get('/api/health', (req, res) => {
  res.json({ ok: true, time: new Date().toISOString() });
});

// DB test
app.get('/api/dbtest', async (req, res) => {
  console.log('[dbtest] Starting...');
  try {
    const r = await pool.query('SELECT 1 as ping');
    console.log('[dbtest] Success:', r.rows);
    res.json({ ok: true, rows: r.rows });
  } catch (err) {
    console.error('[dbtest] Error:', err.message);
    res.status(500).json({ error: err.message });
  }
});

// API: Get products for showroom wings
app.get('/api/showroom/products', async (req, res) => {
  console.log('[products] Request received, vendor:', req.query.vendor, 'limit:', req.query.limit);
  try {
    const vendor = req.query.vendor || 'all';
    const limit = parseInt(req.query.limit) || 13; // per vendor — 13×4=52, front-end takes 50
    let query, params;

    if (vendor === 'all') {
      query = `
        SELECT pattern_name, color_name, mfr_sku as sku, vendor, image_url as image,
               width, collection, material
        FROM (
          (SELECT pattern_name, color_name, mfr_sku, 'Thibaut' as vendor, image_url,
                  width, collection, material
           FROM thibaut_catalog WHERE image_url IS NOT NULL AND image_url != ''
           ORDER BY RANDOM() LIMIT $1)
          UNION ALL
          (SELECT pattern_name, color_name, mfr_sku, 'Schumacher' as vendor, image_url,
                  width, collection, material
           FROM schumacher_catalog WHERE image_url IS NOT NULL AND image_url != ''
           ORDER BY RANDOM() LIMIT $1)
          UNION ALL
          (SELECT pattern_name, color_name, arte_sku as mfr_sku, 'Arte' as vendor, image_url,
                  width, collection, material
           FROM arte_catalog WHERE image_url IS NOT NULL AND image_url != ''
           ORDER BY RANDOM() LIMIT $1)
          UNION ALL
          (SELECT pattern_name, color_name, mfr_sku, 'Elitis' as vendor, image_url,
                  width, collection, material
           FROM elitis_catalog WHERE image_url IS NOT NULL AND image_url != ''
           ORDER BY RANDOM() LIMIT $1)
        ) combined
        ORDER BY vendor, collection, pattern_name
      `;
      params = [limit];
    } else {
      // Map vendor id to table and sku column
      const vendorMap = {
        thibaut: { table: 'thibaut_catalog', sku: 'mfr_sku', name: 'Thibaut' },
        schumacher: { table: 'schumacher_catalog', sku: 'mfr_sku', name: 'Schumacher' },
        arte: { table: 'arte_catalog', sku: 'arte_sku', name: 'Arte' },
        elitis: { table: 'elitis_catalog', sku: 'mfr_sku', name: 'Elitis' }
      };
      const v = vendorMap[vendor];
      if (!v) {
        return res.json({ products: [], total: 0, error: 'Unknown vendor' });
      }
      query = `
        SELECT pattern_name, color_name, ${v.sku} as sku, '${v.name}' as vendor,
               image_url as image, width, collection, material
        FROM ${v.table}
        WHERE image_url IS NOT NULL AND image_url != ''
        ORDER BY collection, pattern_name
        LIMIT $1
      `;
      params = [limit];
    }

    console.log('[products] Executing query...');
    const queryPromise = pool.query(query, params);
    const timeoutPromise = new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Query timeout after 8s')), 8000)
    );
    const result = await Promise.race([queryPromise, timeoutPromise]);
    console.log('[products] Query returned', result.rows.length, 'rows');
    res.json({ products: result.rows, total: result.rows.length });
  } catch (err) {
    console.error('Products API error:', err.message);
    const demoProducts = generateDemoProducts();
    res.json({ products: demoProducts, total: demoProducts.length, fallback: true });
  }
});

// API: Get vendor list
app.get('/api/showroom/vendors', async (req, res) => {
  const vendors = [
    { id: 'thibaut', name: 'Thibaut', bookCount: 12, color: '#1a5276' },
    { id: 'kravet', name: 'Kravet', bookCount: 8, color: '#7d3c98' },
    { id: 'phillipjeffries', name: 'Phillip Jeffries', bookCount: 15, color: '#1e8449' },
    { id: 'york', name: 'York', bookCount: 10, color: '#b9770e' },
    { id: 'schumacher', name: 'Schumacher', bookCount: 6, color: '#922b21' },
    { id: 'cole_son', name: 'Cole & Son', bookCount: 8, color: '#2c3e50' },
    { id: 'winfield', name: 'Winfield Thybony', bookCount: 7, color: '#148f77' },
    { id: 'arte', name: 'Arte International', bookCount: 5, color: '#d4ac0d' },
    { id: 'elitis', name: 'Elitis', bookCount: 6, color: '#a04000' },
    { id: 'brewster', name: 'Brewster', bookCount: 9, color: '#5b2c6f' },
    { id: 'innovations', name: 'Innovations', bookCount: 4, color: '#1b4f72' },
    { id: 'mayaromanoff', name: 'Maya Romanoff', bookCount: 3, color: '#784212' }
  ];
  res.json({ vendors });
});

// API: Proxy Shopify product images
app.get('/api/proxy/image', async (req, res) => {
  const url = req.query.url;
  if (!url) return res.status(400).send('Missing url');
  try {
    const https = require('https');
    const http = require('http');
    const mod = url.startsWith('https') ? https : http;
    mod.get(url, (proxyRes) => {
      res.setHeader('Content-Type', proxyRes.headers['content-type'] || 'image/jpeg');
      res.setHeader('Cache-Control', 'public, max-age=86400');
      proxyRes.pipe(res);
    }).on('error', () => res.status(500).send('Proxy error'));
  } catch (e) {
    res.status(500).send('Error');
  }
});

function generateDemoProducts() {
  const vendors = ['Thibaut', 'Kravet', 'Phillip Jeffries', 'York', 'Schumacher', 'Cole & Son', 'Winfield Thybony', 'Arte', 'Elitis', 'Brewster'];
  const patterns = ['Damask', 'Grasscloth', 'Floral', 'Geometric', 'Stripe', 'Botanical', 'Chinoiserie', 'Texture', 'Paisley', 'Toile'];
  const colors = ['Navy', 'Sage', 'Cream', 'Charcoal', 'Blush', 'Gold', 'Silver', 'Ivory', 'Slate', 'Coral', 'Teal', 'Burgundy'];
  const products = [];

  for (let v = 0; v < vendors.length; v++) {
    for (let p = 0; p < 25; p++) {
      const pattern = patterns[p % patterns.length];
      const color = colors[p % colors.length];
      products.push({
        name: `${pattern} ${color}`,
        color: color,
        sku: `DW-${(v * 1000 + p + 1).toString().padStart(5, '0')}`,
        vendor: vendors[v],
        image: null, // Will use procedural textures
        width_inches: [20.5, 27, 36, 54][p % 4],
        collection: `Collection ${Math.floor(p / 5) + 1}`,
        pattern_name: pattern,
        material: ['Vinyl', 'Non-woven', 'Paper', 'Grasscloth', 'Fabric'][p % 5]
      });
    }
  }
  return products;
}

app.listen(PORT, '0.0.0.0', () => {
  console.log(`DW Showroom Sherman Oaks running on http://0.0.0.0:${PORT}`);
});