fix: public_file_shares Tabelle in db.js, google.de Redirect bei Fehler, saubere Router-Trennung

This commit is contained in:
2026-06-13 12:15:03 +02:00
parent d550972848
commit 1f487cec33
4 changed files with 111 additions and 263 deletions

View File

@@ -571,4 +571,22 @@ if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='xre
if (!db.prepare("SELECT value FROM admin_settings WHERE key='tmdb_token'").get())
db.prepare("INSERT INTO admin_settings (key,value) VALUES ('tmdb_token','')").run();
// Öffentliche Datei/Ordner-Freigabe Links
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='public_file_shares'").get()) {
db.exec(`
CREATE TABLE public_file_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT NOT NULL UNIQUE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
folder_id INTEGER REFERENCES folders(id) ON DELETE CASCADE,
label TEXT,
password_hash TEXT NOT NULL,
expires_at INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
download_count INTEGER NOT NULL DEFAULT 0
)
`);
}
module.exports = db;

View File

@@ -1,42 +1,43 @@
// Öffentliche Endpunkte für Datei/Ordner-Download (kein Login nötig)
const express = require('express');
const bcrypt = require('bcryptjs');
const path = require('path');
const fs = require('fs');
const db = require('../../db');
const router = express.Router();
// Öffentliche Endpunkte kein Login nötig
const express = require('express');
const bcrypt = require('bcryptjs');
const path = require('path');
const fs = require('fs');
const db = require('../../db');
const router = express.Router();
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads';
function getShare(token) {
return db.prepare(`
SELECT s.*, f.name as file_name, f.size as file_size, f.mime_type, f.path as file_path,
fo.name as folder_name
SELECT s.*,
f.name as file_name, f.size as file_size, f.mime_type, f.path as file_path,
fo.name as folder_name
FROM public_file_shares s
LEFT JOIN files f ON f.id = s.file_id
LEFT JOIN folders fo ON fo.id = s.folder_id
WHERE s.token=?
WHERE s.token = ?
`).get(token);
}
function checkExpiry(s) {
function isExpired(s) {
return s.expires_at && s.expires_at < Math.floor(Date.now() / 1000);
}
// GET /:token Metadaten (Name, Passwort nötig, abgelaufen?)
// GET /:token Metadaten
router.get('/:token', (req, res) => {
const s = getShare(req.params.token);
if (!s) return res.status(404).json({ error: 'Link nicht gefunden' });
if (checkExpiry(s)) return res.status(410).json({ error: 'Link abgelaufen' });
if (isExpired(s)) return res.status(410).json({ error: 'Link abgelaufen' });
res.json({
label: s.label,
file_name: s.file_name,
file_size: s.file_size,
mime_type: s.mime_type,
folder_name: s.folder_name,
is_folder: !!s.folder_id,
label: s.label,
file_name: s.file_name,
file_size: s.file_size,
mime_type: s.mime_type,
folder_name: s.folder_name,
is_folder: !!s.folder_id,
needs_password: !!s.password_hash,
expires_at: s.expires_at,
expires_at: s.expires_at,
});
});
@@ -44,11 +45,9 @@ router.get('/:token', (req, res) => {
router.post('/:token/download', async (req, res) => {
const s = getShare(req.params.token);
if (!s || !s.file_id) return res.status(404).json({ error: 'Nicht gefunden' });
if (checkExpiry(s)) return res.status(410).json({ error: 'Link abgelaufen' });
if (s.password_hash) {
const ok = await bcrypt.compare(req.body?.password || '', s.password_hash);
if (!ok) return res.status(403).json({ error: 'Falsches Passwort' });
}
if (isExpired(s)) return res.status(410).json({ error: 'Link abgelaufen' });
const ok = await bcrypt.compare(req.body?.password || '', s.password_hash);
if (!ok) return res.status(403).json({ error: 'Falsches Passwort' });
const filePath = path.join(UPLOAD_DIR, s.file_path);
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Datei nicht gefunden' });
db.prepare('UPDATE public_file_shares SET download_count = download_count + 1 WHERE id=?').run(s.id);
@@ -57,17 +56,15 @@ router.post('/:token/download', async (req, res) => {
fs.createReadStream(filePath).pipe(res);
});
// POST /:token/folder Ordnerinhalt abrufen
// POST /:token/folder Ordnerinhalt
router.post('/:token/folder', async (req, res) => {
const s = getShare(req.params.token);
if (!s || !s.folder_id) return res.status(404).json({ error: 'Nicht gefunden' });
if (checkExpiry(s)) return res.status(410).json({ error: 'Link abgelaufen' });
if (s.password_hash) {
const ok = await bcrypt.compare(req.body?.password || '', s.password_hash);
if (!ok) return res.status(403).json({ error: 'Falsches Passwort' });
}
if (isExpired(s)) return res.status(410).json({ error: 'Link abgelaufen' });
const ok = await bcrypt.compare(req.body?.password || '', s.password_hash);
if (!ok) return res.status(403).json({ error: 'Falsches Passwort' });
const files = db.prepare(`
SELECT id, name, size, mime_type, created_at FROM files
SELECT id, name, size, mime_type FROM files
WHERE folder_id=? AND user_id=? ORDER BY name
`).all(s.folder_id, s.user_id);
db.prepare('UPDATE public_file_shares SET download_count = download_count + 1 WHERE id=?').run(s.id);
@@ -78,14 +75,12 @@ router.post('/:token/folder', async (req, res) => {
router.post('/:token/folder/:fileId', async (req, res) => {
const s = getShare(req.params.token);
if (!s || !s.folder_id) return res.status(404).json({ error: 'Nicht gefunden' });
if (checkExpiry(s)) return res.status(410).json({ error: 'Link abgelaufen' });
if (s.password_hash) {
const ok = await bcrypt.compare(req.body?.password || '', s.password_hash);
if (!ok) return res.status(403).json({ error: 'Falsches Passwort' });
}
if (isExpired(s)) return res.status(410).json({ error: 'Link abgelaufen' });
const ok = await bcrypt.compare(req.body?.password || '', s.password_hash);
if (!ok) return res.status(403).json({ error: 'Falsches Passwort' });
const file = db.prepare('SELECT * FROM files WHERE id=? AND folder_id=? AND user_id=?')
.get(req.params.fileId, s.folder_id, s.user_id);
if (!file) return res.status(404).json({ error: 'Datei nicht im freigegebenen Ordner' });
if (!file) return res.status(404).json({ error: 'Datei nicht gefunden' });
const filePath = path.join(UPLOAD_DIR, file.path);
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Datei nicht gefunden' });
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(file.name)}"`);

View File

@@ -1,40 +1,18 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const path = require('path');
const fs = require('fs');
const db = require('../../db');
// Authentifizierte Endpunkte zum Verwalten öffentlicher Datei-Links
const express = require('express');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const db = require('../../db');
const { authenticate } = require('../../middleware/auth');
const router = express.Router();
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads';
const router = express.Router();
function generateToken() { return crypto.randomBytes(24).toString('base64url'); }
// ── Migration: Tabelle erstellen falls nicht vorhanden ─────────────────────
db.exec(`
CREATE TABLE IF NOT EXISTS public_file_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT NOT NULL UNIQUE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
folder_id INTEGER REFERENCES folders(id) ON DELETE CASCADE,
label TEXT,
password_hash TEXT,
expires_at INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
download_count INTEGER NOT NULL DEFAULT 0,
CHECK (file_id IS NOT NULL OR folder_id IS NOT NULL)
)
`);
// ── GET /api/tools/dateien/public-shares ──────────────────────────────────
// Alle eigenen Shares auflisten
// GET / eigene Shares auflisten
router.get('/', authenticate, (req, res) => {
const shares = db.prepare(`
SELECT s.*,
f.name as file_name, f.size as file_size, f.mime_type,
fo.name as folder_name
SELECT s.*, f.name as file_name, f.size as file_size,
fo.name as folder_name
FROM public_file_shares s
LEFT JOIN files f ON f.id = s.file_id
LEFT JOIN folders fo ON fo.id = s.folder_id
@@ -44,15 +22,13 @@ router.get('/', authenticate, (req, res) => {
res.json({ shares });
});
// ── POST /api/tools/dateien/public-shares ─────────────────────────────────
// Neuen Share erstellen
router.post('/', authenticate, (req, res) => {
// POST / neuen Share erstellen
router.post('/', authenticate, async (req, res) => {
const { file_id, folder_id, password, expires_hours, label } = req.body;
if (!file_id && !folder_id) return res.status(400).json({ error: 'file_id oder folder_id erforderlich' });
if (!password) return res.status(400).json({ error: 'Passwort ist Pflicht' });
if (!expires_hours || expires_hours <= 0) return res.status(400).json({ error: 'Ablaufzeit ist Pflicht' });
if (!password || !password.trim()) return res.status(400).json({ error: 'Passwort ist Pflicht' });
if (!expires_hours || Number(expires_hours) <= 0) return res.status(400).json({ error: 'Ablaufzeit ist Pflicht' });
// Sicherstellen dass die Datei/Ordner dem User gehört
if (file_id) {
const f = db.prepare('SELECT id FROM files WHERE id=? AND user_id=?').get(file_id, req.user.id);
if (!f) return res.status(403).json({ error: 'Keine Berechtigung' });
@@ -63,8 +39,8 @@ router.post('/', authenticate, (req, res) => {
}
const token = generateToken();
const password_hash = password ? bcrypt.hashSync(password, 10) : null;
const expires_at = expires_hours ? Math.floor(Date.now() / 1000) + expires_hours * 3600 : null;
const password_hash = await bcrypt.hash(password.trim(), 10);
const expires_at = Math.floor(Date.now() / 1000) + Number(expires_hours) * 3600;
const r = db.prepare(`
INSERT INTO public_file_shares (token, user_id, file_id, folder_id, label, password_hash, expires_at)
@@ -75,7 +51,7 @@ router.post('/', authenticate, (req, res) => {
res.json({ share, token });
});
// ── DELETE /api/tools/dateien/public-shares/:id ───────────────────────────
// DELETE /:id Share löschen
router.delete('/:id', authenticate, (req, res) => {
const s = db.prepare('SELECT id FROM public_file_shares WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
if (!s) return res.status(404).json({ error: 'Nicht gefunden' });
@@ -83,143 +59,4 @@ router.delete('/:id', authenticate, (req, res) => {
res.json({ ok: true });
});
// ── GET /api/tools/dateien/public-shares/:id/info ─────────────────────────
// Eigene Share-Infos (für Management)
router.get('/:id/info', authenticate, (req, res) => {
const s = db.prepare(`
SELECT s.*, f.name as file_name, fo.name as folder_name
FROM public_file_shares s
LEFT JOIN files f ON f.id = s.file_id
LEFT JOIN folders fo ON fo.id = s.folder_id
WHERE s.id=? AND s.user_id=?
`).get(req.params.id, req.user.id);
if (!s) return res.status(404).json({ error: 'Nicht gefunden' });
res.json({ share: s });
});
// ═══════════════════════════════════════════════════════════════════════════
// ÖFFENTLICHE ENDPUNKTE (kein Login nötig)
// ═══════════════════════════════════════════════════════════════════════════
// ── GET /api/public/file-share/:token ─────────────────────────────────────
// Share-Metadaten abrufen (Name, ob Passwort nötig, abgelaufen?)
router.get('/public/:token', (req, res) => {
const s = db.prepare(`
SELECT s.*, f.name as file_name, f.size as file_size, f.mime_type, f.path as file_path,
fo.name as folder_name
FROM public_file_shares s
LEFT JOIN files f ON f.id = s.file_id
LEFT JOIN folders fo ON fo.id = s.folder_id
WHERE s.token=?
`).get(req.params.token);
if (!s) return res.status(404).json({ error: 'Link nicht gefunden' });
if (s.expires_at && s.expires_at < Math.floor(Date.now() / 1000)) {
return res.status(410).json({ error: 'Link abgelaufen' });
}
res.json({
label: s.label,
file_name: s.file_name,
file_size: s.file_size,
mime_type: s.mime_type,
folder_name: s.folder_name,
is_folder: !!s.folder_id,
needs_password: !!s.password_hash,
expires_at: s.expires_at,
});
});
// ── POST /api/public/file-share/:token/download ───────────────────────────
// Datei herunterladen (mit optionalem Passwort)
router.post('/public/:token/download', async (req, res) => {
const s = db.prepare(`
SELECT s.*, f.name as file_name, f.size as file_size, f.mime_type, f.path as file_path
FROM public_file_shares s
LEFT JOIN files f ON f.id = s.file_id
WHERE s.token=? AND s.file_id IS NOT NULL
`).get(req.params.token);
if (!s) return res.status(404).json({ error: 'Link nicht gefunden oder kein Datei-Link' });
if (s.expires_at && s.expires_at < Math.floor(Date.now() / 1000)) {
return res.status(410).json({ error: 'Link abgelaufen' });
}
if (s.password_hash) {
const { password } = req.body;
if (!password) return res.status(401).json({ error: 'Passwort erforderlich' });
const ok = await bcrypt.compare(password, s.password_hash);
if (!ok) return res.status(403).json({ error: 'Falsches Passwort' });
}
const filePath = path.join(UPLOAD_DIR, s.file_path);
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Datei nicht gefunden' });
db.prepare('UPDATE public_file_shares SET download_count = download_count + 1 WHERE id=?').run(s.id);
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(s.file_name)}"`);
res.setHeader('Content-Type', s.mime_type || 'application/octet-stream');
fs.createReadStream(filePath).pipe(res);
});
// ── POST /api/public/file-share/:token/folder ─────────────────────────────
// Ordnerinhalt abrufen (mit optionalem Passwort)
router.post('/public/:token/folder', async (req, res) => {
const s = db.prepare(`
SELECT s.*, fo.name as folder_name
FROM public_file_shares s
LEFT JOIN folders fo ON fo.id = s.folder_id
WHERE s.token=? AND s.folder_id IS NOT NULL
`).get(req.params.token);
if (!s) return res.status(404).json({ error: 'Link nicht gefunden oder kein Ordner-Link' });
if (s.expires_at && s.expires_at < Math.floor(Date.now() / 1000)) {
return res.status(410).json({ error: 'Link abgelaufen' });
}
if (s.password_hash) {
const { password } = req.body;
if (!password) return res.status(401).json({ error: 'Passwort erforderlich' });
const ok = await bcrypt.compare(password, s.password_hash);
if (!ok) return res.status(403).json({ error: 'Falsches Passwort' });
}
const files = db.prepare(`
SELECT id, name, size, mime_type, created_at FROM files
WHERE folder_id=? AND user_id=?
ORDER BY name
`).all(s.folder_id, s.user_id);
db.prepare('UPDATE public_file_shares SET download_count = download_count + 1 WHERE id=?').run(s.id);
res.json({ folder_name: s.folder_name, files, token: s.token });
});
// ── POST /api/public/file-share/:token/folder/:fileId ─────────────────────
// Einzelne Datei aus freigegebenem Ordner herunterladen
router.post('/public/:token/folder/:fileId', async (req, res) => {
const s = db.prepare(`
SELECT s.* FROM public_file_shares s WHERE s.token=? AND s.folder_id IS NOT NULL
`).get(req.params.token);
if (!s) return res.status(404).json({ error: 'Link nicht gefunden' });
if (s.expires_at && s.expires_at < Math.floor(Date.now() / 1000)) {
return res.status(410).json({ error: 'Link abgelaufen' });
}
if (s.password_hash) {
const { password } = req.body;
if (!password) return res.status(401).json({ error: 'Passwort erforderlich' });
const ok = await bcrypt.compare(password, s.password_hash);
if (!ok) return res.status(403).json({ error: 'Falsches Passwort' });
}
const file = db.prepare('SELECT * FROM files WHERE id=? AND folder_id=? AND user_id=?')
.get(req.params.fileId, s.folder_id, s.user_id);
if (!file) return res.status(404).json({ error: 'Datei nicht im freigegebenen Ordner' });
const filePath = path.join(UPLOAD_DIR, file.path);
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Datei nicht gefunden' });
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(file.name)}"`);
res.setHeader('Content-Type', file.mime_type || 'application/octet-stream');
fs.createReadStream(filePath).pipe(res);
});
module.exports = router;