const express = require('express'); const Database = require('better-sqlite3'); const path = require('path'); const crypto = require('crypto'); const app = express(); const PORT = process.env.PORT || 3000; const DB_PATH = process.env.DB_PATH || '/data/revfulop.db'; // Simple auth config (set SIMPLE_AUTH_PASSWORD env var to enable) const AUTH_PASSWORD = process.env.SIMPLE_AUTH_PASSWORD || ''; const AUTH_ENABLED = AUTH_PASSWORD.length > 0; const SESSION_SECRET = process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex'); // Family members config (can be overridden via FAMILY_MEMBERS env var as JSON) const DEFAULT_MEMBERS = [ { id: "orsi", name: "Orsi", color: "#a15dd8" }, { id: "lili", name: "Lili", color: "#ffe70c" }, { id: "mama", name: "Mama", color: "#513eff" }, ]; let FAMILY_MEMBERS; try { FAMILY_MEMBERS = process.env.FAMILY_MEMBERS ? JSON.parse(process.env.FAMILY_MEMBERS) : DEFAULT_MEMBERS; } catch { FAMILY_MEMBERS = DEFAULT_MEMBERS; } // Init DB const db = new Database(DB_PATH); db.pragma('journal_mode = WAL'); db.exec(` CREATE TABLE IF NOT EXISTS bookings ( id INTEGER PRIMARY KEY AUTOINCREMENT, member_id TEXT NOT NULL, start_date TEXT NOT NULL, end_date TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'planned', created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, member_id TEXT NOT NULL, text TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) ); `); app.use(express.json()); // Session tokens (in-memory, simple approach) const sessions = new Map(); // Simple auth middleware function authMiddleware(req, res, next) { if (!AUTH_ENABLED) return next(); // Skip auth for login endpoint and static assets if (req.path === '/api/login' || req.path === '/api/auth-status') return next(); const token = req.headers['x-auth-token']; if (token && sessions.has(token)) { return next(); } // For API routes, return 401 if (req.path.startsWith('/api/')) { return res.status(401).json({ error: 'Unauthorized' }); } next(); // Let static files through (frontend handles auth UI) } app.use(authMiddleware); // Auth endpoints app.get('/api/auth-status', (req, res) => { res.json({ authEnabled: AUTH_ENABLED }); }); app.post('/api/login', (req, res) => { if (!AUTH_ENABLED) return res.json({ success: true, token: 'none' }); const { password } = req.body; if (password === AUTH_PASSWORD) { const token = crypto.randomBytes(32).toString('hex'); sessions.set(token, { created: Date.now() }); // Clean old sessions (>24h) for (const [t, s] of sessions) { if (Date.now() - s.created > 86400000) sessions.delete(t); } res.json({ success: true, token }); } else { res.status(401).json({ success: false, error: 'Hibás jelszó' }); } }); // Members endpoint app.get('/api/members', (req, res) => { res.json(FAMILY_MEMBERS); }); // Bookings CRUD app.get('/api/bookings', (req, res) => { const bookings = db.prepare('SELECT * FROM bookings ORDER BY start_date').all(); res.json(bookings); }); app.post('/api/bookings', (req, res) => { const { member_id, start_date, end_date, status } = req.body; if (!member_id || !start_date || !end_date) { return res.status(400).json({ error: 'Missing fields' }); } const result = db.prepare( 'INSERT INTO bookings (member_id, start_date, end_date, status) VALUES (?, ?, ?, ?)' ).run(member_id, start_date, end_date, status || 'planned'); res.json({ id: result.lastInsertRowid }); }); app.put('/api/bookings/:id', (req, res) => { const { status } = req.body; db.prepare('UPDATE bookings SET status = ? WHERE id = ?').run(status, req.params.id); res.json({ success: true }); }); app.delete('/api/bookings/:id', (req, res) => { db.prepare('DELETE FROM bookings WHERE id = ?').run(req.params.id); res.json({ success: true }); }); // Comments CRUD app.get('/api/comments', (req, res) => { const comments = db.prepare('SELECT * FROM comments ORDER BY created_at DESC').all(); res.json(comments); }); app.post('/api/comments', (req, res) => { const { member_id, text } = req.body; if (!member_id || !text) { return res.status(400).json({ error: 'Missing fields' }); } const result = db.prepare( 'INSERT INTO comments (member_id, text) VALUES (?, ?)' ).run(member_id, text); res.json({ id: result.lastInsertRowid }); }); // Serve static frontend app.use(express.static(path.join(__dirname, 'public'))); app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); app.listen(PORT, '0.0.0.0', () => { console.log(`Révfülöp Calendar running on port ${PORT}`); console.log(`Auth: ${AUTH_ENABLED ? 'ENABLED (simple password)' : 'DISABLED (public access)'}`); console.log(`Members: ${FAMILY_MEMBERS.map(m => m.name).join(', ')}`); });