Improve folder access admin dashboard
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 20s

This commit is contained in:
Kroonk
2026-05-20 18:44:47 +02:00
parent c211d2bf9d
commit e6a11f2eed
8 changed files with 556 additions and 277 deletions

View File

@@ -90,6 +90,18 @@ function listSubfolders(dir) {
}
}
function getFolderInfo(folder) {
const fullPath = path.join(FULLS_DIR, folder);
const thumbPath = path.join(THUMBS_DIR, folder);
const images = listImagesInDir(fullPath);
return {
name: folder,
image_count: images.length,
thumb_count: listImagesInDir(thumbPath).length,
sample: images[0] || null
};
}
function isSafePath(baseDir, requestedPath) {
const resolvedBase = path.resolve(baseDir);
const resolvedPath = path.resolve(resolvedBase, requestedPath);
@@ -206,29 +218,52 @@ app.get('/images/fulls/', (req, res) => {
const rootImages = listImagesInDir(FULLS_DIR).map(name => ({
name, type: 'file', folder: null
}));
const requestedFolder = req.query.folder;
if (requestedFolder) {
if (!isValidName(requestedFolder)) {
return res.status(400).json({ error: 'Ungueltiger Ordnername' });
}
if (!req.user) {
return res.status(401).json({ error: 'Nicht eingeloggt' });
}
const sendFolderImages = () => {
const folderPath = path.join(FULLS_DIR, requestedFolder);
if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) {
return res.status(404).json({ error: 'Ordner nicht gefunden' });
}
return res.json(listImagesInDir(folderPath).map(name => ({
name, type: 'file', folder: requestedFolder
})));
};
if (req.user.role === 'admin') {
return sendFolderImages();
}
return db.get('SELECT 1 FROM user_folders WHERE user_id = ? AND folder_name = ?',
[req.user.id, requestedFolder], (err, row) => {
if (err || !row) return res.status(403).json({ error: 'Kein Zugriff auf diesen Ordner' });
return sendFolderImages();
});
}
// Nicht eingeloggt: nur Root-Bilder
if (!req.user) return res.json(rootImages);
// Eingeloggt: Startseite bleibt die normale oeffentliche Galerie.
return res.json(rootImages);
});
app.get('/api/folders/available', requireAuth, (req, res) => {
const folders = listSubfolders(FULLS_DIR);
// Admin: Nur Root-Bilder (damit die Startseite sauber bleibt)
if (req.user.role === 'admin') {
return res.json(rootImages);
return res.json(folders.map(getFolderInfo));
}
// User/Client: NUR zugewiesene Ordner (keine Root-Bilder untermischen)
db.all('SELECT folder_name FROM user_folders WHERE user_id = ?', [req.user.id], (err, rows) => {
const assignedFolders = err ? [] : rows.map(r => r.folder_name);
const result = [];
assignedFolders.forEach(folder => {
if (folders.includes(folder)) {
listImagesInDir(path.join(FULLS_DIR, folder)).forEach(name => {
result.push({ name, type: 'file', folder });
});
}
});
return res.json(result);
return res.json(assignedFolders.filter(folder => folders.includes(folder)).map(getFolderInfo));
});
});
@@ -244,8 +279,17 @@ app.get('/api/admin/users', requireAdmin, (req, res) => {
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 => {
db.all('SELECT user_id, folder_name FROM user_folders ORDER BY folder_name', (folderErr, folderRows) => {
const foldersByUser = {};
if (!folderErr && folderRows) {
folderRows.forEach(row => {
if (!foldersByUser[row.user_id]) foldersByUser[row.user_id] = [];
foldersByUser[row.user_id].push(row.folder_name);
});
}
// Ablaufdatum berechnen fuer Clients
const result = rows.map(u => {
const out = { ...u };
if (u.role === 'client' && u.created_at) {
const expires = new Date(u.created_at);
@@ -253,9 +297,11 @@ app.get('/api/admin/users', requireAdmin, (req, res) => {
out.expires_at = expires.toISOString();
out.expired = new Date() > expires;
}
out.folders = foldersByUser[u.id] || [];
return out;
});
res.json(result);
});
res.json(result);
});
});
@@ -298,7 +344,49 @@ app.put('/api/admin/users/:id/password', requireAdmin, (req, res) => {
// Ordner-Verwaltung
app.get('/api/admin/folders', requireAdmin, (req, res) => {
res.json(listSubfolders(FULLS_DIR));
const folders = listSubfolders(FULLS_DIR).map(getFolderInfo);
db.all(
`SELECT uf.folder_name, u.id, u.username, u.role
FROM user_folders uf
JOIN users u ON u.id = uf.user_id
ORDER BY uf.folder_name, u.username`,
(err, rows) => {
const assignedByFolder = {};
if (!err && rows) {
rows.forEach(row => {
if (!assignedByFolder[row.folder_name]) assignedByFolder[row.folder_name] = [];
assignedByFolder[row.folder_name].push({
id: row.id,
username: row.username,
role: row.role || 'client'
});
});
}
res.json(folders.map(folder => ({
...folder,
assigned_users: assignedByFolder[folder.name] || [],
private: true
})));
}
);
});
app.get('/api/admin/folders/:folderName/images', requireAdmin, (req, res) => {
const folderName = req.params.folderName;
if (!isValidName(folderName)) {
return res.status(400).json({ error: 'Ungueltiger Ordnername' });
}
const folderPath = path.join(FULLS_DIR, folderName);
if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) {
return res.status(404).json({ error: 'Ordner nicht gefunden' });
}
res.json(listImagesInDir(folderPath).map(name => ({
name,
folder: folderName,
full_url: '/images/fulls/' + encodeURIComponent(folderName) + '/' + encodeURIComponent(name),
thumb_url: '/images/thumbs/' + encodeURIComponent(folderName) + '/' + encodeURIComponent(name)
})));
});
app.get('/api/admin/assign', requireAdmin, (req, res) => {
@@ -317,7 +405,10 @@ app.get('/api/admin/assign', requireAdmin, (req, res) => {
app.post('/api/admin/assign', requireAdmin, (req, res) => {
const { user_id, folders } = req.body;
if (!user_id) return res.status(400).json({ error: 'user_id erforderlich' });
const folderList = Array.isArray(folders) ? folders : [];
const existingFolders = listSubfolders(FULLS_DIR);
const folderList = Array.isArray(folders)
? folders.filter(name => isValidName(name) && existingFolders.includes(name))
: [];
db.serialize(() => {
db.run('DELETE FROM user_folders WHERE user_id = ?', [user_id]);
const stmt = db.prepare('INSERT INTO user_folders (user_id, folder_name) VALUES (?, ?)');
@@ -408,6 +499,10 @@ app.post('/api/download/selected', (req, res) => {
} else {
// Ordner-Bild: nur wenn User den Ordner zugewiesen hat
if (!req.user) return checkAccess(false);
if (req.user.role === 'admin') {
const filePath = path.join(FULLS_DIR, folder, name);
return checkAccess(fs.existsSync(filePath));
}
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);
@@ -453,30 +548,37 @@ app.get('/api/download/folder/:folderName', (req, res) => {
if (!req.user) {
return res.status(401).json({ error: 'Nicht eingeloggt' });
}
if (req.user.role === 'admin') {
return sendFolderDownload(folderName, res);
}
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();
return sendFolderDownload(folderName, res);
});
});
function sendFolderDownload(folderName, res) {
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);