← back to New Items Dashboard
server.js
201 lines
const express = require('express');
const helmet = require('helmet');
const path = require('path');
const fs = require('fs').promises;
const crypto = require('crypto');
const sqlite3 = require('sqlite3').verbose();
const app = express();
// Security headers via helmet (added 2026-05-04 overnight YOLO loop)
app.use(helmet({ contentSecurityPolicy: false }));
const PORT = process.env.PORT || 7201;
// Snapshot/editor scratch-file 404 guard — never serve *.bak, *.pre-*, *.orig, etc
// even if one accidentally lands under public/
app.use((req, res, next) => {
if (/\.(bak|orig|rej|swp)(\?|$)|\.bak\.|\.pre-|~(\?|$)/i.test(req.path)) {
return res.status(404).send('Not found');
}
next();
});
// ---------------------------------------------------------------------------
// Basic-auth gate (added 2026-05-19 — was publicly exposing vendor + price data)
// SECURITY: rotate before next deploy — set BASIC_AUTH in prod .env to override.
const BASIC_AUTH = process.env.BASIC_AUTH || 'admin:DW2025Secure!';
function safeEqual(a, b) {
const ab = Buffer.from(String(a));
const bb = Buffer.from(String(b));
if (ab.length !== bb.length) return false;
return crypto.timingSafeEqual(ab, bb);
}
app.use((req, res, next) => {
// In dev (NODE_ENV !== 'production') with no explicit BASIC_AUTH set, fail OPEN.
if (process.env.NODE_ENV !== 'production' && !process.env.BASIC_AUTH) return next();
const hdr = req.headers.authorization || '';
if (!hdr.startsWith('Basic ')) {
res.set('WWW-Authenticate', 'Basic realm="new-items-dashboard"');
return res.status(401).send('Authentication required');
}
const supplied = Buffer.from(hdr.slice(6), 'base64').toString();
if (!safeEqual(supplied, BASIC_AUTH)) {
res.set('WWW-Authenticate', 'Basic realm="new-items-dashboard"');
return res.status(401).send('Invalid credentials');
}
next();
});
// ---------------------------------------------------------------------------
// Serve static files
app.use(express.static('public'));
// Shared sort helper — applies `sort` query param to any item list
// in-memory. Frontend persists the user's choice and re-fetches with ?sort=
function applySort(items, sort, type) {
if (!Array.isArray(items) || items.length === 0) return items;
const arr = items.slice();
const cmp = (a, b) => {
const av = (a == null) ? '' : a;
const bv = (b == null) ? '' : b;
if (typeof av === 'number' && typeof bv === 'number') return av - bv;
return String(av).localeCompare(String(bv), undefined, { numeric: true, sensitivity: 'base' });
};
switch (sort) {
case 'title':
return arr.sort((a, b) => cmp(a.title || a.name || '', b.title || b.name || ''));
case 'source':
return arr.sort((a, b) => cmp(a.source || '', b.source || ''));
case 'vendor':
// wines have no vendor field; fall back to source. handbags use brand.
return arr.sort((a, b) => cmp(a.vendor || a.brand || a.source || '', b.vendor || b.brand || b.source || ''));
case 'series':
// wines: vintage; handbags: model
return arr.sort((a, b) => cmp(a.series || a.vintage || a.model || '', b.series || b.vintage || b.model || ''));
case 'sku':
return arr.sort((a, b) => cmp(a.sku || a.id || '', b.sku || a.id || ''));
case 'newest':
default: {
// newest: prefer crawled_at, then timestamp, otherwise leave server order
const hasTs = arr.some(x => x && (x.crawled_at || x.timestamp || x.listed_at));
if (!hasTs) return arr;
return arr.sort((a, b) => {
const at = new Date(a.crawled_at || a.timestamp || a.listed_at || 0).getTime();
const bt = new Date(b.crawled_at || b.timestamp || b.listed_at || 0).getTime();
return bt - at;
});
}
}
}
// API: Get NEW wines
app.get('/api/new-wines', async (req, res) => {
try {
const wineHistoryPath = '/root/Projects/wine-finder/data/seen_wines.json';
// Find latest NEW wines file
const dataDir = '/root/Projects/wine-finder/data';
const files = await fs.readdir(dataDir);
const newWineFiles = files
.filter(f => f.startsWith('NEW-wines-'))
.sort()
.reverse();
if (newWineFiles.length === 0) {
return res.json({ success: true, wines: [], message: 'No crawls yet. First crawl will happen at 6am.' });
}
const latestFile = path.join(dataDir, newWineFiles[0]);
const data = JSON.parse(await fs.readFile(latestFile, 'utf8'));
const wines = applySort(data.newWines || [], req.query.sort, 'wine');
res.json({
success: true,
wines,
count: wines.length,
timestamp: data.timestamp,
summary: data.summary
});
} catch (error) {
console.error('Error reading wines:', error);
res.json({ success: true, wines: [], message: 'No new wines yet. First crawl at 6am.' });
}
});
// API: Get NEW handbags
app.get('/api/new-handbags', (req, res) => {
const dbPath = '/root/Projects/handbag-authentication/data/handbags.db';
const db = new sqlite3.Database(dbPath);
const sql = `
SELECT * FROM listings
WHERE is_new = 1
ORDER BY crawled_at DESC
LIMIT 50
`;
db.all(sql, [], (err, rows) => {
db.close();
if (err) {
console.error('Database error:', err);
return res.json({ success: true, handbags: [], count: 0 });
}
const handbags = applySort(rows || [], req.query.sort, 'handbag');
res.json({
success: true,
handbags,
count: handbags.length
});
});
});
// API: Get stats
app.get('/api/stats', async (req, res) => {
try {
// Wine stats
let wineStats = { total: 0, lastCrawl: null };
try {
const seenWinesPath = '/root/Projects/wine-finder/data/seen_wines.json';
const seenData = JSON.parse(await fs.readFile(seenWinesPath, 'utf8'));
wineStats.total = seenData.totalWines || 0;
wineStats.lastCrawl = seenData.lastUpdated;
} catch (e) {
// File doesn't exist yet
}
// Handbag stats
const handbagStats = await new Promise((resolve) => {
const db = new sqlite3.Database('/root/Projects/handbag-authentication/data/handbags.db');
db.get('SELECT COUNT(*) as total, COUNT(CASE WHEN is_new = 1 THEN 1 END) as new FROM listings', [], (err, row) => {
db.get('SELECT MAX(crawled_at) as lastCrawl FROM listings', [], (err2, row2) => {
db.close();
resolve({
total: row ? row.total : 0,
new: row ? row.new : 0,
lastCrawl: row2 ? row2.lastCrawl : null
});
});
});
});
res.json({
success: true,
wines: wineStats,
handbags: handbagStats
});
} catch (error) {
console.error('Stats error:', error);
res.json({ success: false, error: error.message });
}
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`\n🆕 NEW Items Dashboard running at:`);
console.log(` http://45.61.58.125:${PORT}`);
console.log(`\nShowing latest wines and handbags that just got listed!\n`);
});