const express = require('express'); const bcrypt = require('bcryptjs'); const crypto = require('crypto'); 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'; function generateToken() { return crypto.randomBytes(20).toString('base64url'); } function getOrCreateUploadFolder(userId) { let f = db.prepare("SELECT * FROM folders WHERE user_id=? AND is_upload_folder=1 LIMIT 1").get(userId); if (!f) { const r = db.prepare("INSERT INTO folders (user_id,name,is_upload_folder,created_at) VALUES (?,?,1,datetime('now','localtime'))").run(userId,'Upload'); f = db.prepare('SELECT * FROM folders WHERE id=?').get(r.lastInsertRowid); } return f; } function canUseUploadShare(userId) { const u = db.prepare('SELECT role,allow_upload_share FROM users WHERE id=?').get(userId); return u && (u.role==='admin' || !!u.allow_upload_share); } // ── Eigene Shares ───────────────────────────────────────────────────────────── router.get('/', authenticate, (req, res) => { if (!canUseUploadShare(req.user.id)) return res.status(403).json({ error:'Keine Berechtigung' }); const folder = getOrCreateUploadFolder(req.user.id); const shares = db.prepare(` SELECT s.*, COUNT(l.id) as upload_count FROM upload_shares s LEFT JOIN upload_share_logs l ON l.share_id=s.id WHERE s.user_id=? GROUP BY s.id ORDER BY s.created_at DESC `).all(req.user.id); res.json({ shares, folder }); }); router.post('/', authenticate, (req, res) => { if (!canUseUploadShare(req.user.id)) return res.status(403).json({ error:'Keine Berechtigung' }); const { password, expires_hours=24, max_size_mb=10 } = req.body; if (!password?.trim()) return res.status(400).json({ error:'Passwort erforderlich' }); const folder = getOrCreateUploadFolder(req.user.id); const token = generateToken(); const hash = bcrypt.hashSync(password.trim(), 10); const expires = new Date(Date.now() + Number(expires_hours)*3600000).toISOString(); const r = db.prepare(` INSERT INTO upload_shares (user_id,token,password_hash,expires_at,max_size_mb,folder_id,created_at) VALUES (?,?,?,?,?,?,datetime('now','localtime')) `).run(req.user.id, token, hash, expires, Number(max_size_mb), folder.id); res.json(db.prepare('SELECT * FROM upload_shares WHERE id=?').get(r.lastInsertRowid)); }); router.delete('/:id', authenticate, (req, res) => { const s = db.prepare('SELECT * FROM upload_shares WHERE id=? AND user_id=?').get(req.params.id, req.user.id); if (!s) return res.status(404).json({ error:'Nicht gefunden' }); db.prepare("UPDATE upload_shares SET is_active=0, deactivated_reason='manual' WHERE id=?").run(s.id); res.json({ ok:true }); }); router.delete('/:id/remove', authenticate, (req, res) => { const s = db.prepare('SELECT * FROM upload_shares WHERE id=? AND user_id=?').get(req.params.id, req.user.id); if (!s) return res.status(404).json({ error:'Nicht gefunden' }); db.prepare('DELETE FROM upload_shares WHERE id=?').run(s.id); res.json({ ok:true }); }); router.get('/logs', authenticate, (req, res) => { if (!canUseUploadShare(req.user.id)) return res.status(403).json({ error:'Keine Berechtigung' }); const logs = db.prepare(` SELECT l.*, s.expires_at, s.max_size_mb FROM upload_share_logs l JOIN upload_shares s ON s.id=l.share_id WHERE l.user_id=? ORDER BY l.uploaded_at DESC LIMIT 100 `).all(req.user.id); res.json(logs); }); // ── Admin ───────────────────────────────────────────────────────────────────── router.get('/admin/logs', authenticate, requireAdmin, (req, res) => { const logs = db.prepare(` SELECT l.*, s.expires_at, s.max_size_mb, u.username FROM upload_share_logs l JOIN upload_shares s ON s.id=l.share_id JOIN users u ON u.id=l.user_id ORDER BY l.uploaded_at DESC LIMIT 500 `).all(); res.json(logs); }); router.get('/admin/shares', authenticate, requireAdmin, (req, res) => { const shares = db.prepare(` SELECT s.*, u.username, COUNT(l.id) as upload_count FROM upload_shares s JOIN users u ON u.id=s.user_id LEFT JOIN upload_share_logs l ON l.share_id=s.id GROUP BY s.id ORDER BY s.created_at DESC `).all(); res.json(shares); }); router.post('/admin/permission', authenticate, requireAdmin, (req, res) => { const { user_id, allow } = req.body; db.prepare('UPDATE users SET allow_upload_share=? WHERE id=?').run(allow?1:0, user_id); res.json({ ok:true }); }); // ── Öffentlich: Share-Info ──────────────────────────────────────────────────── router.get('/public/:token', (req, res) => { const s = db.prepare("SELECT id,expires_at,max_size_mb,is_active,failed_attempts,deactivated_reason FROM upload_shares WHERE token=?").get(req.params.token); if (!s || !s.is_active) return res.status(404).json({ error: s?.deactivated_reason==='too_many_attempts' ? 'Link wegen zu vieler Fehlversuche gesperrt' : 'Link ungültig oder deaktiviert' }); if (new Date(s.expires_at) < new Date()) return res.status(410).json({ error:'Link abgelaufen' }); res.json({ ok:true, max_size_mb:s.max_size_mb, expires_at:s.expires_at }); }); // ── Öffentlich: Passwort prüfen ─────────────────────────────────────────────── router.post('/public/:token/verify', (req, res) => { const s = db.prepare("SELECT * FROM upload_shares WHERE token=?").get(req.params.token); if (!s || !s.is_active) return res.status(404).json({ error:'Link ungültig' }); if (new Date(s.expires_at) < new Date()) return res.status(410).json({ error:'Link abgelaufen' }); if (!bcrypt.compareSync(req.body?.password || '', s.password_hash)) { const attempts = (s.failed_attempts||0) + 1; if (attempts >= 3) { // Link sperren db.prepare("UPDATE upload_shares SET is_active=0, failed_attempts=?, deactivated_reason='too_many_attempts' WHERE id=?").run(attempts, s.id); return res.status(401).json({ error:'Zu viele Fehlversuche – Link wurde gesperrt', locked:true }); } db.prepare("UPDATE upload_shares SET failed_attempts=? WHERE id=?").run(attempts, s.id); return res.status(401).json({ error:'Falsches Passwort', attempts_left: 3-attempts }); } // Erfolgreich – Fehlversuche zurücksetzen db.prepare("UPDATE upload_shares SET failed_attempts=0 WHERE id=?").run(s.id); res.json({ ok:true, max_size_mb:s.max_size_mb, expires_at:s.expires_at }); }); // ── Öffentlich: Upload ──────────────────────────────────────────────────────── router.post('/public/:token/upload', (req, res) => { const s = db.prepare("SELECT * FROM upload_shares WHERE token=?").get(req.params.token); if (!s || !s.is_active) return res.status(404).json({ error:'Link ungültig' }); if (new Date(s.expires_at) < new Date()) return res.status(410).json({ error:'Link abgelaufen' }); const pw = req.headers['x-upload-password'] || ''; if (!bcrypt.compareSync(pw, s.password_hash)) return res.status(401).json({ error:'Nicht autorisiert' }); const storage = multer.diskStorage({ destination: UPLOAD_DIR }); const upload = multer({ storage, limits:{ fileSize: s.max_size_mb*1024*1024 } }).single('file'); upload(req, res, err => { if (err) { if (err.code==='LIMIT_FILE_SIZE') return res.status(413).json({ error:`Datei zu groß (max ${s.max_size_mb} MB)` }); return res.status(500).json({ error:err.message }); } if (!req.file) return res.status(400).json({ error:'Keine Datei' }); try { db.prepare(`INSERT INTO files (user_id,filename,originalname,mimetype,size,folder_id,created_at) VALUES (?,?,?,?,?,?,datetime('now','localtime'))`).run(s.user_id,req.file.filename,req.file.originalname,req.file.mimetype,req.file.size,s.folder_id); db.prepare(`INSERT INTO upload_share_logs (share_id,user_id,originalname,size,ip_address,uploaded_at) VALUES (?,?,?,?,?,datetime('now','localtime'))`).run(s.id,s.user_id,req.file.originalname,req.file.size,req.ip||''); res.json({ ok:true, filename:req.file.originalname }); } catch(e) { try { fs.unlinkSync(path.join(UPLOAD_DIR, req.file.filename)); } catch {} res.status(500).json({ error:e.message }); } }); }); module.exports = router;