Files
dickendock/backend/src/tools/dateien/upload-share.js

172 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;