Merge GitHub updates into Gitea master
All checks were successful
Build & Push Docker Image / build-and-push (push) Successful in 1m53s

This commit is contained in:
Tom Misch
2026-05-11 13:03:58 +02:00
18 changed files with 1768 additions and 88 deletions

View File

@@ -17,7 +17,15 @@ db.serialize(() => {
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// Migration: created_at hinzufuegen falls Tabelle schon existiert
// Migration: Spalten hinzufuegen falls Tabelle schon existiert (alte DB ohne role/created_at)
db.run(`ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'client'`, (err) => {
// Ignoriere Fehler wenn Spalte schon existiert
if (!err) {
// Alte Admins (die einzigen User vor dem Update) als admin markieren
db.run(`UPDATE users SET role = 'admin' WHERE id = 1`);
console.log('Migration: role-Spalte hinzugefuegt, User ID 1 als admin gesetzt');
}
});
db.run(`ALTER TABLE users ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP`, (err) => {
// Ignoriere Fehler wenn Spalte schon existiert
});

View File

@@ -5,6 +5,7 @@ const bcrypt = require('bcryptjs');
const fs = require('fs');
const path = require('path');
const { execFile } = require('child_process');
const archiver = require('archiver');
const db = require('./database');
const app = express();
@@ -19,7 +20,7 @@ const DEV_FULLS_DIR = path.join(__dirname, '../images/fulls');
const DEV_THUMBS_DIR = path.join(__dirname, '../images/thumbs');
const FULLS_DIR = process.env.FULLS_DIR || (fs.existsSync(DEFAULT_FULLS_DIR) ? DEFAULT_FULLS_DIR : DEV_FULLS_DIR);
const THUMBS_DIR = process.env.THUMBS_DIR || (fs.existsSync(DEFAULT_THUMBS_DIR) ? DEFAULT_THUMBS_DIR : DEV_THUMBS_DIR);
const SYNC_IMAGE = process.env.SYNC_IMAGE || 'ghcr.io/kroonk/photography:latest';
const SYNC_IMAGE = process.env.SYNC_IMAGE || 'git.mischlabs.de/mrdiderot/photography:latest';
const SYNC_CONTAINER = process.env.SYNC_CONTAINER || 'photography-website';
const IMAGE_RE = /\.(jpg|jpeg|png|gif|webp)$/i;
let syncInProgress = false;
@@ -235,7 +236,14 @@ app.get('/images/fulls/', (req, res) => {
app.get('/api/admin/users', requireAdmin, (req, res) => {
db.all('SELECT id, username, role, created_at FROM users', (err, rows) => {
if (err) return res.json([]);
if (err) {
console.error('User-Abfrage fehlgeschlagen:', err.message);
// Fallback: einfache Abfrage ohne neue Spalten
return db.all('SELECT * FROM users', (err2, rows2) => {
if (err2) return res.status(500).json({ error: 'DB Error: ' + err2.message });
res.json(rows2.map(u => ({ id: u.id, username: u.username, role: u.role || 'client', created_at: u.created_at || null })));
});
}
// Ablaufdatum berechnen fuer Clients
const result = rows.map(u => {
const out = { ...u };
@@ -354,6 +362,121 @@ app.post('/api/admin/sync', requireAdmin, (req, res) => {
});
});
// ==================== DOWNLOAD API ====================
function isValidName(name) {
return name && !name.includes('/') && !name.includes('\\') && !name.includes('..');
}
app.post('/api/download/selected', (req, res) => {
const { images } = req.body;
if (!Array.isArray(images) || images.length === 0) {
return res.status(400).json({ error: 'Keine Bilder ausgewaehlt' });
}
// Alle Bilder validieren und Zugriff pruefen
let pending = images.length;
const verified = [];
let responded = false;
images.forEach((img, idx) => {
if (responded) return;
const name = img.name;
const folder = img.folder || null;
// Dateiname + Ordnername validieren
if (!isValidName(name) || !IMAGE_RE.test(name) || (folder && !isValidName(folder))) {
responded = true;
return res.status(400).json({ error: 'Ungueltiger Dateiname' });
}
const checkAccess = (ok) => {
if (responded) return;
if (!ok) {
responded = true;
return res.status(403).json({ error: 'Kein Zugriff auf: ' + name });
}
verified[idx] = img;
pending--;
if (pending === 0) sendFiles();
};
if (!folder) {
// Root-Bild: fuer alle zugaenglich
const filePath = path.join(FULLS_DIR, name);
checkAccess(fs.existsSync(filePath));
} else {
// Ordner-Bild: nur wenn User den Ordner zugewiesen hat
if (!req.user) return checkAccess(false);
db.get('SELECT 1 FROM user_folders WHERE user_id = ? AND folder_name = ?',
[req.user.id, folder], (err, row) => {
if (err || !row) return checkAccess(false);
const filePath = path.join(FULLS_DIR, folder, name);
checkAccess(fs.existsSync(filePath));
});
}
});
function sendFiles() {
if (verified.length <= 3) {
// Einzelne Downloads: URLs zurueckgeben
const urls = verified.map(img => {
const prefix = img.folder ? encodeURIComponent(img.folder) + '/' : '';
return '/images/fulls/' + prefix + encodeURIComponent(img.name);
});
return res.json({ mode: 'individual', urls });
}
// 4+ Bilder: ZIP streamen
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', 'attachment; filename="mischkomposition-auswahl.zip"');
const archive = archiver('zip', { zlib: { level: 5 } });
archive.on('error', () => {
if (!res.headersSent) res.status(500).json({ error: 'ZIP-Fehler' });
});
archive.pipe(res);
verified.forEach(img => {
const filePath = img.folder
? path.join(FULLS_DIR, img.folder, img.name)
: path.join(FULLS_DIR, img.name);
archive.file(filePath, { name: img.name });
});
archive.finalize();
}
});
app.get('/api/download/folder/:folderName', (req, res) => {
const folderName = req.params.folderName;
if (!isValidName(folderName)) {
return res.status(400).json({ error: 'Ungueltiger Ordnername' });
}
if (!req.user) {
return res.status(401).json({ error: 'Nicht eingeloggt' });
}
db.get('SELECT 1 FROM user_folders WHERE user_id = ? AND folder_name = ?',
[req.user.id, folderName], (err, row) => {
if (err || !row) {
return res.status(403).json({ error: 'Kein Zugriff auf diesen Ordner' });
}
const folderPath = path.join(FULLS_DIR, folderName);
const images = listImagesInDir(folderPath);
if (images.length === 0) {
return res.status(404).json({ error: 'Keine Bilder im Ordner' });
}
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', 'attachment; filename="' + encodeURIComponent(folderName) + '.zip"');
const archive = archiver('zip', { zlib: { level: 5 } });
archive.on('error', () => {
if (!res.headersSent) res.status(500).json({ error: 'ZIP-Fehler' });
});
archive.pipe(res);
images.forEach(name => {
archive.file(path.join(folderPath, name), { name });
});
archive.finalize();
});
});
// ==================== STATIC FILES ====================
app.get(/^\/images\/(fulls|thumbs)\/(.+)$/, serveProtectedImage);