commit a587ed69e6cd80e0b3bd9deb09288bc8221b5445 Author: authentik Default Admin Date: Sat Feb 7 07:11:29 2026 +0000 Upload files to "public" diff --git a/public/Dockerfile b/public/Dockerfile new file mode 100644 index 0000000..3547023 --- /dev/null +++ b/public/Dockerfile @@ -0,0 +1,17 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json ./ +RUN npm ci --production + +COPY server.js ./ +COPY public/ ./public/ + +RUN mkdir -p /data + +EXPOSE 3000 + +USER node + +CMD ["node", "server.js"] diff --git a/public/package.json b/public/package.json new file mode 100644 index 0000000..808609a --- /dev/null +++ b/public/package.json @@ -0,0 +1,13 @@ +{ + "name": "revfulop-calendar", + "version": "1.0.0", + "description": "Family vacation house booking calendar", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "better-sqlite3": "^11.7.0", + "express": "^4.21.0" + } +} diff --git a/public/server.js b/public/server.js new file mode 100644 index 0000000..7616f97 --- /dev/null +++ b/public/server.js @@ -0,0 +1,160 @@ +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: "#E07A5F" }, + { id: "papa", name: "Papa", color: "#3D405B" }, + { id: "mama", name: "Mama", color: "#81B29A" }, + { id: "tesa", name: "Tesa", color: "#F2CC8F" }, + { id: "balint", name: "Bálint", color: "#7B9EA8" }, +]; + +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(', ')}`); +});