diff --git a/backend/src/db.js b/backend/src/db.js index 9d35203..8255f0c 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -521,46 +521,13 @@ if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='cal `); } -// ── Upload-Freigaben (externer Upload-Link) ─────────────────────────────────── -if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='upload_shares'").get()) { - db.exec(` - CREATE TABLE upload_shares ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - token TEXT NOT NULL UNIQUE, - password_hash TEXT NOT NULL, - expires_at DATETIME NOT NULL, - max_size_mb INTEGER NOT NULL DEFAULT 10, - folder_id INTEGER REFERENCES folders(id) ON DELETE SET NULL, - is_active INTEGER NOT NULL DEFAULT 1, - created_at DATETIME DEFAULT NULL - ) - `); -} -if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='upload_share_logs'").get()) { - db.exec(` - CREATE TABLE upload_share_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - share_id INTEGER NOT NULL REFERENCES upload_shares(id) ON DELETE CASCADE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - originalname TEXT NOT NULL, - size INTEGER NOT NULL DEFAULT 0, - ip_address TEXT NOT NULL DEFAULT '', - uploaded_at DATETIME DEFAULT NULL - ) - `); -} -// allow_upload_share Recht pro User +// upload_shares Fehlversuche-Tracking { - const userCols = db.pragma('table_info(users)').map(c => c.name); - if (!userCols.includes('allow_upload_share')) - db.exec('ALTER TABLE users ADD COLUMN allow_upload_share INTEGER DEFAULT 0'); -} -// Upload-Ordner Flag auf Folders-Tabelle -{ - const folderCols = db.pragma('table_info(folders)').map(c => c.name); - if (!folderCols.includes('is_upload_folder')) - db.exec('ALTER TABLE folders ADD COLUMN is_upload_folder INTEGER DEFAULT 0'); + const usCols = db.pragma('table_info(upload_shares)').map(c => c.name); + if (!usCols.includes('failed_attempts')) + db.exec('ALTER TABLE upload_shares ADD COLUMN failed_attempts INTEGER DEFAULT 0'); + if (!usCols.includes('deactivated_reason')) + db.exec("ALTER TABLE upload_shares ADD COLUMN deactivated_reason TEXT DEFAULT NULL"); } module.exports = db; diff --git a/backend/src/index.js b/backend/src/index.js index 9f71d54..9541f51 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -66,10 +66,10 @@ fs.readdirSync(toolsDir, { withFileTypes: true }) } }); -app.use('/api/tools/snippets', require('./tools/snippets/routes')); -app.use('/api/tools/qrcodes', require('./tools/qrcodes/routes')); +app.use('/api/tools/snippets', require('./tools/snippets/routes')); +app.use('/api/tools/qrcodes', require('./tools/qrcodes/routes')); app.use('/api/upload-shares', require('./tools/dateien/upload-share')); -app.use('/api/search', require('./routes/search')); +app.use('/api/search', require('./routes/search')); const PUBLIC = path.join(__dirname, '../public'); diff --git a/backend/src/tools/dateien/upload-share.js b/backend/src/tools/dateien/upload-share.js index 38e6e27..7955aed 100644 --- a/backend/src/tools/dateien/upload-share.js +++ b/backend/src/tools/dateien/upload-share.js @@ -10,88 +10,78 @@ const router = express.Router(); const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads'; -// ── Hilfsfunktionen ─────────────────────────────────────────────────────────── -function generateToken() { - return crypto.randomBytes(20).toString('base64url'); -} +function generateToken() { return crypto.randomBytes(20).toString('base64url'); } function getOrCreateUploadFolder(userId) { - let folder = db.prepare( - "SELECT * FROM folders WHERE user_id=? AND is_upload_folder=1 LIMIT 1" - ).get(userId); - if (!folder) { - const r = db.prepare( - "INSERT INTO folders (user_id, name, is_upload_folder, created_at) VALUES (?,?,1,datetime('now','localtime'))" - ).run(userId, 'Upload-Ordner'); - folder = db.prepare('SELECT * FROM folders WHERE id=?').get(r.lastInsertRowid); + 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-Ordner'); + f = db.prepare('SELECT * FROM folders WHERE id=?').get(r.lastInsertRowid); } - return folder; + return f; } function canUseUploadShare(userId) { - const user = db.prepare('SELECT role, allow_upload_share FROM users WHERE id=?').get(userId); - return user && (user.role === 'admin' || !!user.allow_upload_share); + 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 verwalten ─────────────────────────────────────────────────── +// ── Eigene Shares ───────────────────────────────────────────────────────────── router.get('/', authenticate, (req, res) => { - if (!canUseUploadShare(req.user.id)) return res.status(403).json({ error: 'Keine Berechtigung' }); + 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 + 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' }); + 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' }); + 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 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) + 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)); + const share = db.prepare('SELECT * FROM upload_shares WHERE id=?').get(r.lastInsertRowid); + res.json({ ...share, url:`/u/${token}` }); }); 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 WHERE id=?').run(s.id); - res.json({ ok: true }); + 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.get('/logs', authenticate, (req, res) => { - if (!canUseUploadShare(req.user.id)) return res.status(403).json({ error: 'Keine Berechtigung' }); + 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 + WHERE l.user_id=? ORDER BY l.uploaded_at DESC LIMIT 100 `).all(req.user.id); res.json(logs); }); -// ── Admin: alle Logs sehen ──────────────────────────────────────────────────── +// ── 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 + ORDER BY l.uploaded_at DESC LIMIT 500 `).all(); res.json(logs); }); @@ -99,83 +89,74 @@ router.get('/admin/logs', authenticate, requireAdmin, (req, res) => { 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 + 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 + GROUP BY s.id ORDER BY s.created_at DESC `).all(); res.json(shares); }); -// ── Admin: Berechtigung pro User setzen ────────────────────────────────────── 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 }); + res.json({ ok:true }); }); -// ── Öffentlicher Endpunkt: Infos zum Share ──────────────────────────────────── +// ── Öffentlich: Share-Info ──────────────────────────────────────────────────── router.get('/public/:token', (req, res) => { - const s = db.prepare( - "SELECT id, expires_at, max_size_mb, is_active FROM upload_shares WHERE token=?" - ).get(req.params.token); - if (!s || !s.is_active) return res.status(404).json({ error: '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 }); + 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 }); }); -// ── Öffentlicher Endpunkt: Passwort prüfen ──────────────────────────────────── +// ── Ö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)) - return res.status(401).json({ error: 'Falsches Passwort' }); - res.json({ ok: true, max_size_mb: s.max_size_mb, expires_at: s.expires_at }); + 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 }); }); -// ── Öffentlicher Upload ─────────────────────────────────────────────────────── +// ── Ö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 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' }); - // Passwort im Header prüfen const pw = req.headers['x-upload-password'] || ''; - if (!bcrypt.compareSync(pw, s.password_hash)) - return res.status(401).json({ error: 'Nicht autorisiert' }); + if (!bcrypt.compareSync(pw, s.password_hash)) return res.status(401).json({ error:'Nicht autorisiert' }); - const maxBytes = s.max_size_mb * 1024 * 1024; - const storage = multer.diskStorage({ destination: UPLOAD_DIR }); - const upload = multer({ - storage, - limits: { fileSize: maxBytes }, - }).single('file'); + 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 (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' }); - + if (!req.file) return res.status(400).json({ error:'Keine Datei' }); try { - const r = 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 }); + 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 }); + res.status(500).json({ error:e.message }); } }); }); diff --git a/backend/upload-page.html b/backend/upload-page.html index 11231dc..832db7d 100644 --- a/backend/upload-page.html +++ b/backend/upload-page.html @@ -7,140 +7,181 @@
Dateien hochladen
+Dateien senden
-