184 lines
6.6 KiB
JavaScript
184 lines
6.6 KiB
JavaScript
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}`);
|
|
});
|