← back to Wine Finder
server.js
159 lines
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const helmet = require('helmet');
const path = require('path');
const rateLimit = require('express-rate-limit');
const scraperRoutes = require('./routes/scraper');
const authRoutes = require('./routes/auth');
const historyRoutes = require('./routes/history');
const verificationRoutes = require('./routes/verification');
const app = express();
const PORT = process.env.PORT || 9222;
// Security middleware - CSP without upgrade-insecure-requests for HTTP access
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "https:", "http:"],
connectSrc: ["'self'"],
upgradeInsecureRequests: null
}
}
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);
// Body parsing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Session configuration
const SESSION_SECRET = process.env.SESSION_SECRET;
if (!SESSION_SECRET) {
throw new Error('SESSION_SECRET env var required (no fallback for production safety) — audit 2026-05-04');
}
app.use(session({
secret: SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // set to true if using HTTPS
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// Guard: never serve snapshot/backup files even if accidentally committed under public/
app.use((req, res, next) => {
if (/\.(bak|pre-[\w.-]+|orig|swp|tmp)(\.[\w.-]+)?$/i.test(req.path) || /\/\.(pre-|bak)/i.test(req.path)) {
return res.status(404).json({ success: false, error: 'Not found' });
}
next();
});
// Static files - disable caching
app.use(express.static(path.join(__dirname, 'public'), {
etag: false,
lastModified: false,
setHeaders: (res) => {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
}
}));
// Authentication middleware
const requireAuth = (req, res, next) => {
if (req.session.authenticated) {
next();
} else {
// Check if this is an API request (JSON)
if (req.headers['content-type'] === 'application/json' ||
req.headers['accept']?.includes('application/json')) {
return res.status(401).json({
success: false,
error: 'Authentication required',
redirectTo: '/login'
});
}
// For page requests, redirect to login
res.redirect('/login');
}
};
// Routes
app.use('/auth', authRoutes);
app.use('/api/scraper', scraperRoutes); // Public for verify page camera search
app.use('/api/history', requireAuth, historyRoutes);
app.use('/api/verification', verificationRoutes); // Public for verify page
// Serve login page
app.get('/login', (req, res) => {
if (req.session.authenticated) {
return res.redirect('/');
}
res.sendFile(path.join(__dirname, 'public', 'login.html'));
});
// Serve main app (protected)
app.get('/', requireAuth, (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Logout route
app.get('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
console.error('Session destruction error:', err);
}
res.redirect('/login');
});
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error('Error:', err);
res.status(500).json({
success: false,
error: 'Internal server error'
});
});
// 404 handler
app.use((req, res) => {
res.status(404).json({
success: false,
error: 'Not found'
});
});
// Start server
app.listen(PORT, '0.0.0.0', () => {
console.log(`🍷 Red Thunder Wine Tracker running on port ${PORT}`);
console.log(`📊 Access at: http://localhost:${PORT}`);
console.log(`🔐 Login with username: ${process.env.ADMIN_USER || 'admin'}`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM signal received: closing HTTP server');
process.exit(0);
});
process.on('SIGINT', () => {
console.log('SIGINT signal received: closing HTTP server');
process.exit(0);
});