const express = require('express'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const db = require('../../db'); const { authenticate, requireAdmin } = require('../../middleware/auth'); const router = express.Router(); const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads'; if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true }); const getSetting = key => db.prepare('SELECT value FROM admin_settings WHERE key=?').get(key)?.value; const storage = multer.diskStorage({ destination: UPLOAD_DIR, filename: (req, file, cb) => cb(null, `${Date.now()}-${Math.random().toString(36).slice(2)}${path.extname(file.originalname)}`), }); const getUpload = () => multer({ storage, limits: { fileSize: parseInt(getSetting('file_max_size_mb') || '50') * 1024 * 1024 }, fileFilter: (req, file, cb) => { const allowed = (getSetting('file_allowed_ext') || '').split(',').map(e => e.trim().toLowerCase()).filter(Boolean); if (!allowed.length) return cb(null, true); const ext = path.extname(file.originalname).toLowerCase(); allowed.includes(ext) ? cb(null, true) : cb(new Error(`Dateityp nicht erlaubt: ${ext}`)); }, }); // Walk folder tree upward to check if any ancestor is shared with user function isInSharedFolder(folderId, uid) { let cur = folderId; while (cur) { if (db.prepare('SELECT id FROM folder_shares WHERE folder_id=? AND shared_with=?').get(cur, uid)) return true; const p = db.prepare('SELECT parent_id FROM folders WHERE id=?').get(cur); cur = p?.parent_id || null; } return false; } const enrichFolder = (folder) => ({ ...folder, fileCount: db.prepare('SELECT COUNT(*) c FROM files WHERE folder_id=?').get(folder.id).c, subCount: db.prepare('SELECT COUNT(*) c FROM folders WHERE parent_id=?').get(folder.id).c, totalSize: db.prepare('SELECT COALESCE(SUM(size),0) s FROM files WHERE folder_id=?').get(folder.id).s, }); // ── Ordner ──────────────────────────────────────────────────────────────────── router.get('/folders', authenticate, (req, res) => { const uid = req.user.id; const parentId = req.query.parent_id ? parseInt(req.query.parent_id) : null; // Own folders in this directory const own = parentId ? db.prepare('SELECT * FROM folders WHERE user_id=? AND parent_id=? ORDER BY name').all(uid, parentId) : db.prepare('SELECT * FROM folders WHERE user_id=? AND parent_id IS NULL ORDER BY name').all(uid); // Shared folders let shared = []; if (!parentId) { shared = db.prepare(` SELECT f.*, u.username as owner, GROUP_CONCAT(su.username) as shared_with_names, MIN(fs.created_at) as shared_at FROM folders f JOIN users u ON u.id=f.user_id JOIN folder_shares fs ON fs.folder_id=f.id JOIN users su ON su.id=fs.shared_with WHERE fs.shared_with=? GROUP BY f.id ORDER BY f.name `).all(uid); } else if (isInSharedFolder(parentId, uid)) { const parentFolder = db.prepare('SELECT * FROM folders WHERE id=?').get(parentId); if (parentFolder) { shared = db.prepare('SELECT f.*, u.username as owner FROM folders f JOIN users u ON u.id=f.user_id WHERE f.parent_id=? AND f.user_id!=? ORDER BY f.name').all(parentId, uid); } } // Folders shared BY me (root only) const sharedByMe = parentId ? [] : db.prepare(` SELECT DISTINCT f.*, u.username as owner, GROUP_CONCAT(su.username) as shared_with_names, MIN(fs.created_at) as shared_at FROM folders f JOIN users u ON u.id=f.user_id JOIN folder_shares fs ON fs.folder_id=f.id JOIN users su ON su.id=fs.shared_with WHERE f.user_id=? GROUP BY f.id ORDER BY f.name `).all(uid); res.json({ own: own.map(enrichFolder), shared: shared.map(enrichFolder), sharedByMe: sharedByMe.map(enrichFolder) }); }); router.post('/folders', authenticate, (req, res) => { const { name, parent_id } = req.body; if (!name?.trim()) return res.status(400).json({ error: 'Name erforderlich' }); const parentId = parent_id ? parseInt(parent_id) : null; if (parentId) { const parent = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(parentId, req.user.id); if (!parent) return res.status(404).json({ error: 'Überordner nicht gefunden' }); } const r = db.prepare('INSERT INTO folders (user_id,name,parent_id) VALUES (?,?,?)').run(req.user.id, name.trim(), parentId); res.json(enrichFolder(db.prepare('SELECT * FROM folders WHERE id=?').get(r.lastInsertRowid))); }); router.delete('/folders/:id', authenticate, (req, res) => { const folder = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(req.params.id, req.user.id); if (!folder) return res.status(404).json({ error: 'Nicht gefunden' }); // Delete all files inside const files = db.prepare('SELECT * FROM files WHERE folder_id=? AND user_id=?').all(folder.id, req.user.id); for (const f of files) { try { fs.unlinkSync(path.join(UPLOAD_DIR, f.filename)); } catch {} } db.prepare('DELETE FROM files WHERE folder_id=? AND user_id=?').run(folder.id, req.user.id); db.prepare('DELETE FROM folders WHERE id=?').run(folder.id); res.json({ success: true }); }); router.get('/folders/:id/shares', authenticate, (req, res) => { const folder = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(req.params.id, req.user.id); if (!folder) return res.status(404).json({ error: 'Nicht gefunden' }); res.json(db.prepare('SELECT u.id,u.username,fs.created_at as shared_at FROM folder_shares fs JOIN users u ON u.id=fs.shared_with WHERE fs.folder_id=?').all(folder.id)); }); router.post('/folders/:id/share', authenticate, (req, res) => { const folder = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(req.params.id, req.user.id); if (!folder) return res.status(404).json({ error: 'Nicht gefunden' }); const target = db.prepare('SELECT * FROM users WHERE username=?').get(req.body.username); if (!target) return res.status(404).json({ error: 'Benutzer nicht gefunden' }); if (target.id === req.user.id) return res.status(400).json({ error: 'Kann nicht mit dir selbst teilen' }); db.prepare('INSERT OR IGNORE INTO folder_shares (folder_id,shared_by,shared_with) VALUES (?,?,?)').run(folder.id, req.user.id, target.id); res.json({ success: true }); }); router.delete('/folders/:id/share/:userId', authenticate, (req, res) => { const folder = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(req.params.id, req.user.id); if (!folder) return res.status(404).json({ error: 'Nicht gefunden' }); db.prepare('DELETE FROM folder_shares WHERE folder_id=? AND shared_with=?').run(folder.id, req.params.userId); res.json({ success: true }); }); // ── Dateien ─────────────────────────────────────────────────────────────────── router.get('/', authenticate, (req, res) => { const uid = req.user.id; const folderId = req.query.folder_id ? parseInt(req.query.folder_id) : null; // Own files in this directory const own = folderId ? db.prepare('SELECT f.*, u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.user_id=? AND f.folder_id=? ORDER BY f.created_at DESC').all(uid, folderId) : db.prepare('SELECT f.*, u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.user_id=? AND f.folder_id IS NULL ORDER BY f.created_at DESC').all(uid); // Files shared BY me const sharedByMe = folderId ? [] : db.prepare(` SELECT DISTINCT f.*, u.username as owner, GROUP_CONCAT(su.username) as shared_with_names, MIN(s.created_at) as shared_at, MAX(s.accessed_at) as accessed_at, MAX(CASE WHEN s.password_hash IS NOT NULL THEN 1 ELSE 0 END) as has_password FROM files f JOIN users u ON u.id=f.user_id JOIN file_shares s ON s.file_id=f.id JOIN users su ON su.id=s.shared_with WHERE f.user_id=? GROUP BY f.id ORDER BY f.created_at DESC `).all(uid); // Files shared with me let shared = []; if (!folderId) { shared = db.prepare(` SELECT f.*, u.username as owner, sb.username as shared_by_name, CASE WHEN s.password_hash IS NOT NULL THEN 1 ELSE 0 END as has_password FROM files f JOIN users u ON u.id=f.user_id JOIN file_shares s ON s.file_id=f.id JOIN users sb ON sb.id=s.shared_by WHERE s.shared_with=? ORDER BY f.created_at DESC `).all(uid); } else if (isInSharedFolder(folderId, uid)) { const folder = db.prepare('SELECT * FROM folders WHERE id=?').get(folderId); if (folder) { shared = db.prepare('SELECT f.*, u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.folder_id=? AND f.user_id=? ORDER BY f.created_at DESC').all(folderId, folder.user_id); } } res.json({ own, shared, sharedByMe }); }); router.post('/', authenticate, (req, res) => { const uid = req.user.id; const maxSizeMb = parseInt(getSetting('file_max_size_mb') || '50'); const totalLimitMb= parseInt(getSetting('file_total_size_mb') || '500'); const usedBytes = db.prepare('SELECT COALESCE(SUM(size),0) AS s FROM files WHERE user_id=?').get(uid).s; if (usedBytes >= totalLimitMb * 1024 * 1024) return res.status(400).json({ error: `Gesamtlimit von ${totalLimitMb} MB erreicht` }); getUpload().single('file')(req, res, err => { if (err) return res.status(400).json({ error: err.message }); if (!req.file) return res.status(400).json({ error: 'Keine Datei' }); const folderId = req.body.folder_id ? parseInt(req.body.folder_id) : null; const r = db.prepare('INSERT INTO files (user_id,filename,originalname,mimetype,size,folder_id) VALUES (?,?,?,?,?,?)') .run(uid, req.file.filename, req.file.originalname, req.file.mimetype||'application/octet-stream', req.file.size, folderId); res.json(db.prepare('SELECT f.*,u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.id=?').get(r.lastInsertRowid)); }); }); // ── 3D-Modelle Hilfsendpoints ───────────────────────────────────────────────── // Sucht einen Ordner ohne ihn anzulegen – gibt null zurück wenn nicht gefunden router.get('/find-folder', authenticate, (req, res) => { const uid = req.user.id; const { name, parent_id } = req.query; if (!name?.trim()) return res.json(null); const parentId = parent_id ? parseInt(parent_id) : null; const folder = parentId ? db.prepare('SELECT * FROM folders WHERE user_id=? AND name=? AND parent_id=?').get(uid, name.trim(), parentId) : db.prepare('SELECT * FROM folders WHERE user_id=? AND name=? AND parent_id IS NULL').get(uid, name.trim()); res.json(folder ? enrichFolder(folder) : null); }); // Stellt sicher dass ein Ordner existiert (Name + optionaler parent), gibt ihn zurück router.post('/ensure-folder', authenticate, (req, res) => { const uid = req.user.id; const { name, parent_id } = req.body; if (!name?.trim()) return res.status(400).json({ error: 'Name erforderlich' }); const parentId = parent_id ? parseInt(parent_id) : null; // Suche existierenden Ordner const existing = parentId ? db.prepare('SELECT * FROM folders WHERE user_id=? AND name=? AND parent_id=?').get(uid, name.trim(), parentId) : db.prepare('SELECT * FROM folders WHERE user_id=? AND name=? AND parent_id IS NULL').get(uid, name.trim()); if (existing) return res.json(enrichFolder(existing)); const r = db.prepare('INSERT INTO folders (user_id,name,parent_id) VALUES (?,?,?)').run(uid, name.trim(), parentId); res.json(enrichFolder(db.prepare('SELECT * FROM folders WHERE id=?').get(r.lastInsertRowid))); }); // Upload 3D-Dateien (ohne Ext-Beschränkung, für den Kalkulator) const upload3d = multer({ storage, limits: { fileSize: parseInt(process.env.MAX_3D_SIZE_MB || '200') * 1024 * 1024 }, }); router.post('/upload-3d', authenticate, (req, res) => { upload3d.array('files', 20)(req, res, err => { if (err) return res.status(400).json({ error: err.message }); if (!req.files?.length) return res.status(400).json({ error: 'Keine Dateien' }); const uid = req.user.id; const folderId = req.body.folder_id ? parseInt(req.body.folder_id) : null; const inserted = []; for (const f of req.files) { const r = db.prepare('INSERT INTO files (user_id,filename,originalname,mimetype,size,folder_id) VALUES (?,?,?,?,?,?)') .run(uid, f.filename, f.originalname, f.mimetype || 'application/octet-stream', f.size, folderId); inserted.push(r.lastInsertRowid); } res.json({ uploaded: inserted.length }); }); }); router.get('/storage', authenticate, (req, res) => { const uid = req.user.id; const used = db.prepare('SELECT COALESCE(SUM(size),0) AS s FROM files WHERE user_id=?').get(uid).s; const count = db.prepare('SELECT COUNT(*) AS c FROM files WHERE user_id=?').get(uid).c; const maxSizeMb = parseInt(getSetting('file_max_size_mb') || '50'); const totalLimitMb = parseInt(getSetting('file_total_size_mb') || '500'); res.json({ used, count, maxSizeMb, totalLimitMb }); }); router.get('/:id/download', authenticate, async (req, res) => { const uid = req.user.id; const file = db.prepare('SELECT * FROM files WHERE id=?').get(req.params.id); if (!file) return res.status(404).json({ error: 'Nicht gefunden' }); const isOwner = file.user_id === uid; const share = db.prepare('SELECT * FROM file_shares WHERE file_id=? AND shared_with=?').get(file.id, uid); const inSharedDir = file.folder_id && isInSharedFolder(file.folder_id, uid); if (!isOwner && !share && !inSharedDir) return res.status(403).json({ error: 'Kein Zugriff' }); // Passwortprüfung für Shares (Owner braucht kein Passwort) if (!isOwner && share?.password_hash) { const bcrypt = require('bcryptjs'); const pw = req.query.password || req.headers['x-share-password'] || ''; const ok = pw && await bcrypt.compare(pw, share.password_hash); if (!ok) return res.status(403).json({ error: 'Falsches Passwort', passwordRequired: !pw // true = noch kein PW eingegeben, false = falsches PW }); } const fp = path.join(UPLOAD_DIR, file.filename); if (!fs.existsSync(fp)) return res.status(404).json({ error: 'Datei fehlt' }); // Zugriff tracken (immer aktualisieren damit es sicher gesetzt wird) if (share) { db.prepare("UPDATE file_shares SET accessed_at = datetime('now','localtime') WHERE file_id=? AND shared_with=?") .run(file.id, uid); } res.download(fp, file.originalname); }); router.delete('/:id', authenticate, (req, res) => { const file = db.prepare('SELECT * FROM files WHERE id=? AND user_id=?').get(req.params.id, req.user.id); if (!file) return res.status(404).json({ error: 'Nicht gefunden' }); try { fs.unlinkSync(path.join(UPLOAD_DIR, file.filename)); } catch {} db.prepare('DELETE FROM files WHERE id=?').run(file.id); res.json({ success: true }); }); router.get('/:id/shares', authenticate, (req, res) => { const file = db.prepare('SELECT * FROM files WHERE id=? AND user_id=?').get(req.params.id, req.user.id); if (!file) return res.status(404).json({ error: 'Nicht gefunden' }); res.json(db.prepare(` SELECT u.id, u.username, s.created_at as shared_at, s.accessed_at, CASE WHEN s.password_hash IS NOT NULL THEN 1 ELSE 0 END as has_password FROM file_shares s JOIN users u ON u.id=s.shared_with WHERE s.file_id=? `).all(file.id)); }); router.post('/:id/share', authenticate, async (req, res) => { const file = db.prepare('SELECT * FROM files WHERE id=? AND user_id=?').get(req.params.id, req.user.id); if (!file) return res.status(404).json({ error: 'Nicht gefunden' }); const target = db.prepare('SELECT * FROM users WHERE username=?').get(req.body.username); if (!target) return res.status(404).json({ error: 'Benutzer nicht gefunden' }); if (target.id === req.user.id) return res.status(400).json({ error: 'Kann nicht mit dir selbst teilen' }); let passwordHash = null; if (req.body.password) { const bcrypt = require('bcryptjs'); passwordHash = await bcrypt.hash(req.body.password, 10); } const existing = db.prepare('SELECT id FROM file_shares WHERE file_id=? AND shared_with=?').get(file.id, target.id); if (existing) { db.prepare('UPDATE file_shares SET password_hash=?, accessed_at=NULL WHERE file_id=? AND shared_with=?') .run(passwordHash, file.id, target.id); } else { db.prepare('INSERT INTO file_shares (file_id,shared_by,shared_with,password_hash) VALUES (?,?,?,?)') .run(file.id, req.user.id, target.id, passwordHash); } res.json({ success: true }); }); router.delete('/:id/share/:userId', authenticate, (req, res) => { const file = db.prepare('SELECT * FROM files WHERE id=? AND user_id=?').get(req.params.id, req.user.id); if (!file) return res.status(404).json({ error: 'Nicht gefunden' }); db.prepare('DELETE FROM file_shares WHERE file_id=? AND shared_with=?').run(file.id, req.params.userId); res.json({ success: true }); }); // Settings router.get('/settings', authenticate, requireAdmin, (req, res) => { const s = db.prepare('SELECT * FROM admin_settings').all(); const obj = {}; for (const r of s) obj[r.key] = r.value; res.json(obj); }); router.put('/settings', authenticate, requireAdmin, (req, res) => { for (const key of ['file_max_size_mb','file_total_size_mb','file_allowed_ext']) { if (req.body[key] !== undefined) db.prepare('INSERT OR REPLACE INTO admin_settings (key,value) VALUES (?,?)').run(key, String(req.body[key])); } res.json({ success: true }); }); module.exports = router;