Upload files to "public"
This commit is contained in:
@@ -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"]
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(', ')}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user