157 lines
4.5 KiB
JavaScript
157 lines
4.5 KiB
JavaScript
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);
|
|
});
|