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