const express = require('express'); const session = require('express-session'); const path = require('path'); const fs = require('fs'); // Load .env if present try { const envFile = fs.readFileSync(path.join(__dirname, '..', '.env'), 'utf8'); envFile.split('\n').forEach(line => { const match = line.match(/^([^#=]+)=(.*)$/); if (match) { process.env[match[1].trim()] = match[2].trim(); } }); } catch (e) { /* .env file is optional */ } // Ensure data dirs exist const dataDir = path.dirname(process.env.DB_PATH || './data/loot-hunt.db'); const uploadsDir = process.env.UPLOADS_DIR || './data/uploads'; [dataDir, uploadsDir].forEach(dir => { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); }); // Initialize database (async — schema creation happens in background) const db = require('./config/database'); const { loadUser } = require('./middleware/auth'); // ─── SQLite Session Store ───────────────────────────────── class SQLiteSessionStore extends session.Store { constructor() { super(); } get(sid, callback) { try { const row = db.prepare('SELECT sess FROM sessions WHERE sid = ? AND expired > datetime(\'now\')').get(sid); if (row) { callback(null, JSON.parse(row.sess)); } else { callback(null, null); } } catch (err) { callback(err); } } set(sid, sess, callback) { try { const maxAge = sess.cookie && sess.cookie.maxAge ? sess.cookie.maxAge : 86400000; const expiredDate = new Date(Date.now() + maxAge).toISOString(); const sessStr = JSON.stringify(sess); // upsert db.prepare('DELETE FROM sessions WHERE sid = ?').run(sid); db.prepare('INSERT INTO sessions (sid, sess, expired) VALUES (?, ?, ?)').run(sid, sessStr, expiredDate); callback(null); } catch (err) { callback(err); } } destroy(sid, callback) { try { db.prepare('DELETE FROM sessions WHERE sid = ?').run(sid); callback(null); } catch (err) { callback(err); } } // Clean up expired sessions periodically touch(sid, sess, callback) { this.set(sid, sess, callback); } } // ─── Start app once DB is ready ─────────────────────────── async function start() { await db.ready; const app = express(); const PORT = process.env.PORT || 3000; // View engine app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); // Body parsing app.use(express.urlencoded({ extended: true })); app.use(express.json()); // Static files app.use(express.static(path.join(__dirname, '..', 'public'))); app.use('/uploads', express.static(path.resolve(uploadsDir))); // Sessions app.use(session({ store: new SQLiteSessionStore(), secret: process.env.SESSION_SECRET || 'loot-hunt-dev-secret-change-me', resave: false, saveUninitialized: false, cookie: { maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days httpOnly: true, secure: process.env.NODE_ENV === 'production' && process.env.TRUST_PROXY === 'true' } })); // Trust proxy if behind nginx if (process.env.TRUST_PROXY === 'true') { app.set('trust proxy', 1); } // Load current user into all views app.use(loadUser); // Flash-like messages via session app.use((req, res, next) => { res.locals.flash = req.session.flash || null; delete req.session.flash; next(); }); // Routes app.use('/auth', require('./routes/auth')); app.use('/admin', require('./routes/admin')); app.use('/loot', require('./routes/loot')); app.use('/', require('./routes/hunts')); // QR Scanner app.get('/scanner', (req, res) => { res.render('scanner', { title: 'QR Scanner' }); }); // Home page app.get('/', (req, res) => { const { Hunts, Scans } = require('./models'); const hunts = Hunts.getAll(); const recentActivity = Scans.getRecentActivity(5); res.render('home', { title: 'Loot Hunt', hunts, recentActivity }); }); // 404 handler app.use((req, res) => { res.status(404).render('error', { title: 'Not Found', message: 'The page you are looking for does not exist.' }); }); // Error handler app.use((err, req, res, next) => { console.error(err.stack); res.status(500).render('error', { title: 'Error', message: 'Something went wrong.' }); }); app.listen(PORT, '0.0.0.0', () => { console.log(`🎯 Loot Hunt running on port ${PORT}`); }); } start().catch(err => { console.error('Failed to start:', err); process.exit(1); });