Upload-Share: Route registriert, Fehlversuche-Limit, PW kopieren, bessere Fehlerbehandlung

This commit is contained in:
2026-06-05 23:25:45 +02:00
parent 7e19a6580e
commit e7ebf2d831
5 changed files with 187 additions and 189 deletions

View File

@@ -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;

View File

@@ -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');

View File

@@ -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 });
}
});
});