172 lines
8.7 KiB
JavaScript
172 lines
8.7 KiB
JavaScript
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;
|