require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }); const express = require('express'); const cors = require('cors'); const path = require('path'); const fs = require('fs'); const os = require('os'); const session = require('express-session'); const app = express(); const PORT = process.env.PORT || 3001; app.use(cors({ origin: process.env.NODE_ENV === 'production' ? false : 'http://localhost:5173', credentials: true, })); app.use(express.json()); app.use(session({ secret: process.env.APP_SESSION_SECRET || 'dev-secret-change-in-production', resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax', maxAge: 24 * 60 * 60 * 1000, // 24 hours }, })); const pricingData = JSON.parse( fs.readFileSync(path.join(__dirname, 'data', 'pricing.json'), 'utf-8') ); // Auth middleware function requireAuth(req, res, next) { if (req.session && req.session.authenticated) { return next(); } res.status(401).json({ error: 'Unauthorized' }); } // Login app.post('/api/login', (req, res) => { const { password } = req.body; if (password === process.env.APP_PASSWORD) { req.session.authenticated = true; return res.json({ success: true }); } res.status(401).json({ error: 'Invalid password' }); }); // Logout app.post('/api/logout', (req, res) => { req.session.destroy((err) => { if (err) { return res.status(500).json({ error: 'Failed to logout' }); } res.clearCookie('connect.sid'); res.json({ success: true }); }); }); // Check auth status app.get('/api/auth/status', (req, res) => { res.json({ authenticated: !!(req.session && req.session.authenticated) }); }); // Protected API routes app.get('/api/pricing', requireAuth, (req, res) => { res.json(pricingData); }); app.get('/api/pricing/:category', requireAuth, (req, res) => { const { category } = req.params; const validCategories = ['operators', 'groundLoopStyles', 'groundLoopSizes', 'loopDetectors', 'groundLoopTypes', 'accessControl', 'remoteButtonOptions']; if (validCategories.includes(category)) { return res.json(pricingData[category]); } res.status(400).json({ error: `Invalid category. Use: ${validCategories.join(', ')}` }); }); // Serve static files (SPA loads without auth — auth is API-only) const clientDist = path.join(__dirname, '..', 'client', 'dist'); if (fs.existsSync(clientDist)) { app.use(express.static(clientDist)); app.get('*', (req, res) => { res.sendFile(path.join(clientDist, 'index.html')); }); } function getLocalIP() { const ifaces = os.networkInterfaces(); for (const name of Object.keys(ifaces)) { for (const iface of ifaces[name]) { if (iface.family === 'IPv4' && !iface.internal) return iface.address; } } return 'localhost'; } app.listen(PORT, '0.0.0.0', () => { const ip = getLocalIP(); console.log(`\n Local: http://localhost:${PORT}`); console.log(` Network: http://${ip}:${PORT}`); console.log(` Login: http://localhost:${PORT}/login\n`); });