← back to Dw War Room
serve.cjs
186 lines
// Simple static server with Basic Auth for War Room
// Includes Shopify search proxy endpoint for showroom live search
// Uses Shopify GraphQL Admin API for fuzzy product search
const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');
const url = require('url');
const PORT = 4060;
const USER = 'admin';
const PASS = 'DWSecure2024!';
const DIST = path.join(__dirname, 'dist');
// Shopify API config (Admin API — server-side only)
const SHOPIFY_STORE = 'designer-laboratory-sandbox.myshopify.com';
const SHOPIFY_TOKEN = (process.env.SHOPIFY_ADMIN_TOKEN || '');
const SHOPIFY_API_VERSION = '2024-01';
const MIME = {
'.html': 'text/html',
'.js': 'application/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.woff2': 'font/woff2',
'.woff': 'font/woff',
};
/**
* Proxy Shopify product search via GraphQL — keeps API token server-side.
* Uses GraphQL for proper fuzzy search (REST title= requires exact match).
* GET /api/shopify/search?q=QUERY&limit=10
* Returns: { products: [{ id, title, vendor, product_type, handle, images, variants, tags }] }
*/
function handleShopifySearch(query, limit, res) {
if (!query || query.length < 2) {
res.writeHead(400, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Query must be at least 2 characters' }));
}
limit = Math.min(parseInt(limit, 10) || 10, 20);
// GraphQL query — searches across title, vendor, tags, etc.
const gqlQuery = JSON.stringify({
query: `{
products(first: ${limit}, query: "${query.replace(/"/g, '\\"')}") {
edges {
node {
id
title
vendor
productType
handle
tags
featuredImage {
url
altText
}
images(first: 1) {
edges {
node {
url
altText
}
}
}
variants(first: 1) {
edges {
node {
price
sku
}
}
}
}
}
}
}`
});
const options = {
hostname: SHOPIFY_STORE,
path: `/admin/api/${SHOPIFY_API_VERSION}/graphql.json`,
method: 'POST',
headers: {
'X-Shopify-Access-Token': SHOPIFY_TOKEN,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(gqlQuery),
},
};
const req2 = https.request(options, (shopifyRes) => {
let data = '';
shopifyRes.on('data', (chunk) => { data += chunk; });
shopifyRes.on('end', () => {
try {
const gqlResponse = JSON.parse(data);
const edges = gqlResponse?.data?.products?.edges || [];
// Transform GraphQL response to REST-like format for frontend compatibility
const products = edges.map(({ node }) => ({
id: parseInt(node.id.replace('gid://shopify/Product/', ''), 10),
title: node.title,
vendor: node.vendor,
product_type: node.productType,
handle: node.handle,
tags: node.tags ? node.tags.join(', ') : '',
images: node.images?.edges?.length
? node.images.edges.map(e => ({ src: e.node.url }))
: node.featuredImage
? [{ src: node.featuredImage.url }]
: [],
variants: node.variants?.edges?.length
? node.variants.edges.map(e => ({ price: e.node.price, sku: e.node.sku || '' }))
: [{ price: '0.00', sku: '' }],
}));
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'max-age=30',
});
res.end(JSON.stringify({ products }));
} catch (parseErr) {
console.error('[Shopify Search Proxy] Parse error:', parseErr.message);
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to parse Shopify response', products: [] }));
}
});
});
req2.on('error', (err) => {
console.error('[Shopify Search Proxy] Error:', err.message);
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Shopify API request failed', products: [] }));
});
req2.write(gqlQuery);
req2.end();
}
const server = http.createServer((req, res) => {
// Basic Auth check
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Basic ')) {
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DW War Room"' });
return res.end('Unauthorized');
}
const [u, p] = Buffer.from(auth.slice(6), 'base64').toString().split(':');
if (u !== USER || p !== PASS) {
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DW War Room"' });
return res.end('Unauthorized');
}
// Parse URL
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
// API routes — handle before static file serving
if (pathname === '/api/shopify/search' && req.method === 'GET') {
return handleShopifySearch(parsedUrl.query.q, parsedUrl.query.limit, res);
}
// Serve static files
let filePath = path.join(DIST, pathname === '/' ? 'index.html' : pathname);
if (!fs.existsSync(filePath)) filePath = path.join(DIST, 'index.html');
const ext = path.extname(filePath);
const mime = MIME[ext] || 'application/octet-stream';
try {
const content = fs.readFileSync(filePath);
res.writeHead(200, { 'Content-Type': mime });
res.end(content);
} catch {
res.writeHead(404);
res.end('Not found');
}
});
server.listen(PORT, '0.0.0.0', () => {
console.log(`[War Room] Serving on port ${PORT} with auth + Shopify search proxy (GraphQL)`);
});