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