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; // UI config (all overridable via env vars, no rebuild needed) const UI_CONFIG = { fontSize: process.env.UI_FONT_SIZE || '15', // base font size in px titleSize: process.env.UI_TITLE_SIZE || '32', // main title size in px calendarSize: process.env.UI_CALENDAR_SIZE || '14', // calendar day number size in px buttonSize: process.env.UI_BUTTON_SIZE || '14', // button/input font size in px siteName: process.env.UI_SITE_NAME || 'Nyaraló Naptár', siteSubtitle: process.env.UI_SITE_SUBTITLE || 'Révfülöp · Balaton', loginTagline: process.env.UI_LOGIN_TAGLINE || 'Ahol a család találkozik,
ahol a nyár kezdődik.', loginSubtitle: process.env.UI_LOGIN_SUBTITLE || 'A révfülöpi nyaraló foglalási naptára', }; // Family members config (can be overridable via FAMILY_MEMBERS env var as JSON) const DEFAULT_MEMBERS = [ { id: "katinka", name: "Katinka", color: "#513eff" }, { id: "orsi", name: "Orsi", color: "#a15dd8" }, { id: "lili", name: "Lili", color: "#ffe70c" }, { id: "bazsi", name: "Bazsi", color: "#2db84a" }, ]; 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: token -> { created, memberId } const sessions = new Map(); // Auth helper: get logged-in member from token function getMember(req) { const token = req.headers['x-auth-token']; if (!token) return null; const session = sessions.get(token); if (!session) return null; return session.memberId; } // Auth middleware function authMiddleware(req, res, next) { if (!AUTH_ENABLED) return next(); // Skip auth for login, auth-status, and members endpoints if (req.path === '/api/login' || req.path === '/api/auth-status' || req.path === '/api/members') return next(); const token = req.headers['x-auth-token']; if (token && sessions.has(token)) { return next(); } if (req.path.startsWith('/api/')) { return res.status(401).json({ error: 'Unauthorized' }); } next(); } 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', memberId: null }); const { password, memberId } = req.body; // Validate member exists if (!memberId || !FAMILY_MEMBERS.find(m => m.id === memberId)) { return res.status(400).json({ success: false, error: 'Válassz egy családtagot' }); } if (password === AUTH_PASSWORD) { const token = crypto.randomBytes(32).toString('hex'); sessions.set(token, { created: Date.now(), memberId }); // Clean old sessions (>7 days) for (const [t, s] of sessions) { if (Date.now() - s.created > 7 * 86400000) sessions.delete(t); } res.json({ success: true, token, memberId }); } else { res.status(401).json({ success: false, error: 'Hibás jelszó' }); } }); // Who am I endpoint app.get('/api/me', (req, res) => { const memberId = getMember(req); if (!memberId) return res.json({ memberId: null }); const member = FAMILY_MEMBERS.find(m => m.id === memberId); res.json({ memberId, member }); }); // Members endpoint app.get('/api/members', (req, res) => { res.json(FAMILY_MEMBERS); }); // UI config endpoint app.get('/api/config', (req, res) => { res.json(UI_CONFIG); }); // 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 { start_date, end_date, status } = req.body; // Use logged-in member if auth enabled, otherwise accept from body const member_id = AUTH_ENABLED ? getMember(req) : req.body.member_id; 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 { text } = req.body; const member_id = AUTH_ENABLED ? getMember(req) : req.body.member_id; 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 }); }); app.delete('/api/comments/:id', (req, res) => { const memberId = AUTH_ENABLED ? getMember(req) : req.body.member_id; const comment = db.prepare('SELECT * FROM comments WHERE id = ?').get(req.params.id); if (!comment) return res.status(404).json({ error: 'Not found' }); if (AUTH_ENABLED && comment.member_id !== memberId) { return res.status(403).json({ error: 'Csak a saját hozzászólásod törölheted' }); } db.prepare('DELETE FROM comments WHERE id = ?').run(req.params.id); res.json({ success: true }); }); // 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(', ')}`); console.log(`UI: font=${UI_CONFIG.fontSize}px, title=${UI_CONFIG.titleSize}px, calendar=${UI_CONFIG.calendarSize}px`); });