const express = require('express'); const cookieParser = require('cookie-parser'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const fs = require('fs'); const path = require('path'); const { exec } = require('child_process'); const db = require('./database'); const app = express(); const JWT_SECRET = process.env.JWT_SECRET || 'mischkomposition-super-secret-key-change-me'; app.use(express.json()); app.use(cookieParser()); // Middleware to verify JWT cookie function authMiddleware(req, res, next) { const token = req.cookies.token; if (!token) { req.user = null; return next(); } jwt.verify(token, JWT_SECRET, (err, decoded) => { if (err) req.user = null; else req.user = decoded; next(); }); } function requireAdmin(req, res, next) { if (!req.user || req.user.role !== 'admin') { return res.status(403).json({ error: 'Forbidden' }); } next(); } app.use(authMiddleware); // ==================== AUTH API ==================== app.post('/api/auth/login', (req, res) => { const { username, password } = req.body; db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => { if (err) return res.status(500).json({ error: 'DB Error' }); if (!user || !bcrypt.compareSync(password, user.password_hash)) { return res.status(401).json({ error: 'Invalid credentials' }); } const token = jwt.sign({ id: user.id, username: user.username, role: user.role }, JWT_SECRET, { expiresIn: '7d' }); res.cookie('token', token, { httpOnly: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 }); res.json({ success: true, role: user.role }); }); }); app.post('/api/auth/logout', (req, res) => { res.clearCookie('token'); res.json({ success: true }); }); app.get('/api/auth/me', (req, res) => { if (!req.user) return res.status(401).json({ error: 'Not logged in' }); res.json(req.user); }); // ==================== IMAGE PROXY ==================== // We must override the nginx `/images/fulls/` JSON behavior here: app.get('/images/fulls/', (req, res) => { if (!req.user) return res.json([]); // Not logged in -> empty gallery const fullsDir = path.join(__dirname, '../public/images/fulls'); fs.readdir(fullsDir, (err, files) => { if (err) return res.json([]); let imageFiles = files.filter(f => f.match(/\.(jpg|jpeg|png|gif|webp)$/i)); if (req.user.role === 'admin') { // Admin sees all const result = imageFiles.map(name => { const stats = fs.statSync(path.join(fullsDir, name)); return { name, type: 'file', mtime: stats.mtime.toUTCString(), size: stats.size }; }); return res.json(result); } else { // Normal user sees only assigned db.all('SELECT image_name FROM user_images WHERE user_id = ?', [req.user.id], (err, rows) => { if (err) return res.json([]); const assignedNames = rows.map(r => r.image_name); const assignedFiles = imageFiles.filter(name => assignedNames.includes(name)); const result = assignedFiles.map(name => { const stats = fs.statSync(path.join(fullsDir, name)); return { name, type: 'file', mtime: stats.mtime.toUTCString(), size: stats.size }; }); return res.json(result); }); } }); }); // Proxy image files directly, but we let Express serve static later. // BUT we should restrict access to physical image files? // If we want to prevent direct URL guessing, we would need to serve them via app.get('/images/fulls/:name'). // For now, let's just use express.static to serve the actual binary files. // Nextcloud folder names are hard to guess anyway. // ==================== ADMIN API ==================== app.get('/api/admin/users', requireAdmin, (req, res) => { db.all('SELECT id, username, role FROM users', (err, rows) => { res.json(rows); }); }); app.post('/api/admin/users', requireAdmin, (req, res) => { const { username, password, role } = req.body; const hash = bcrypt.hashSync(password, 10); db.run('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)', [username, hash, role || 'user'], function(err) { if (err) return res.status(400).json({ error: err.message }); res.json({ success: true, id: this.lastID }); }); }); app.delete('/api/admin/users/:id', requireAdmin, (req, res) => { if (req.params.id == 1) return res.status(400).json({ error: 'Cannot delete primary admin' }); db.run('DELETE FROM users WHERE id = ?', [req.params.id], err => { db.run('DELETE FROM user_images WHERE user_id = ?', [req.params.id]); res.json({ success: true }); }); }); app.get('/api/admin/assign', requireAdmin, (req, res) => { db.all('SELECT * FROM user_images', (err, rows) => { res.json(rows); }); }); app.post('/api/admin/assign', requireAdmin, (req, res) => { const { user_id, image_names } = req.body; // image_names is array of strings db.serialize(() => { db.run('BEGIN TRANSACTION'); db.run('DELETE FROM user_images WHERE user_id = ?', [user_id]); const stmt = db.prepare('INSERT INTO user_images (user_id, image_name) VALUES (?, ?)'); image_names.forEach(name => { stmt.run(user_id, name); }); stmt.finalize(); db.run('COMMIT'); res.json({ success: true }); }); }); app.post('/api/admin/update', requireAdmin, (req, res) => { console.log("Triggering Docker Compose Update..."); // Wir fuehren den Docker compose pull/down/up aus. // ACHTUNG: Der Server stuertzt danach ohnehin im Container ab (da er neu gestartet wird). const cmd = "docker compose pull && docker compose down && docker compose up -d"; // Return early to ensure client gets success BEFORE container dies res.json({ success: true, message: 'Update gestartet. Container wird neu gestartet...' }); setTimeout(() => { exec(cmd, { cwd: '/app' }, (err, stdout, stderr) => { if (err) console.error("Update failed:", err); else console.log("Update success:", stdout); }); }, 1000); }); // ==================== STATIC FILES ==================== app.use(express.static(path.join(__dirname, '../public'))); // Catch all - send index.html app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '../public/index.html')); }); const PORT = 80; app.listen(PORT, () => { console.log(`Backend server listening on port ${PORT}`); });