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()) 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(); 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; module.exports = db;

View File

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

View File

@@ -1,39 +1,17 @@
// Authentifizierte Endpunkte zum Verwalten öffentlicher Datei-Links
const express = require('express'); const express = require('express');
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
const crypto = require('crypto'); const crypto = require('crypto');
const path = require('path');
const fs = require('fs');
const db = require('../../db'); const db = require('../../db');
const { authenticate } = require('../../middleware/auth'); const { authenticate } = require('../../middleware/auth');
const router = express.Router(); const router = express.Router();
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads';
function generateToken() { return crypto.randomBytes(24).toString('base64url'); } function generateToken() { return crypto.randomBytes(24).toString('base64url'); }
// ── Migration: Tabelle erstellen falls nicht vorhanden ───────────────────── // GET / eigene Shares auflisten
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
router.get('/', authenticate, (req, res) => { router.get('/', authenticate, (req, res) => {
const shares = db.prepare(` const shares = db.prepare(`
SELECT s.*, SELECT s.*, f.name as file_name, f.size as file_size,
f.name as file_name, f.size as file_size, f.mime_type,
fo.name as folder_name fo.name as folder_name
FROM public_file_shares s FROM public_file_shares s
LEFT JOIN files f ON f.id = s.file_id LEFT JOIN files f ON f.id = s.file_id
@@ -44,15 +22,13 @@ router.get('/', authenticate, (req, res) => {
res.json({ shares }); res.json({ shares });
}); });
// ── POST /api/tools/dateien/public-shares ───────────────────────────────── // POST / neuen Share erstellen
// Neuen Share erstellen router.post('/', authenticate, async (req, res) => {
router.post('/', authenticate, (req, res) => {
const { file_id, folder_id, password, expires_hours, label } = req.body; 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 (!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 (!password || !password.trim()) 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 (!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) { if (file_id) {
const f = db.prepare('SELECT id FROM files WHERE id=? AND user_id=?').get(file_id, req.user.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' }); if (!f) return res.status(403).json({ error: 'Keine Berechtigung' });
@@ -63,8 +39,8 @@ router.post('/', authenticate, (req, res) => {
} }
const token = generateToken(); const token = generateToken();
const password_hash = password ? bcrypt.hashSync(password, 10) : null; const password_hash = await bcrypt.hash(password.trim(), 10);
const expires_at = expires_hours ? Math.floor(Date.now() / 1000) + expires_hours * 3600 : null; const expires_at = Math.floor(Date.now() / 1000) + Number(expires_hours) * 3600;
const r = db.prepare(` const r = db.prepare(`
INSERT INTO public_file_shares (token, user_id, file_id, folder_id, label, password_hash, expires_at) 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 }); res.json({ share, token });
}); });
// ── DELETE /api/tools/dateien/public-shares/:id ─────────────────────────── // DELETE /:id Share löschen
router.delete('/:id', authenticate, (req, res) => { 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); 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' }); if (!s) return res.status(404).json({ error: 'Nicht gefunden' });
@@ -83,143 +59,4 @@ router.delete('/:id', authenticate, (req, res) => {
res.json({ ok: true }); 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; module.exports = router;

View File

@@ -2911,44 +2911,34 @@ function PublicFileShare({ token }) {
box: { background:'#1a1d2e', border:'1px solid rgba(255,255,255,0.08)', borderRadius:16, padding:32, maxWidth:480, width:'100%' }, box: { background:'#1a1d2e', border:'1px solid rgba(255,255,255,0.08)', borderRadius:16, padding:32, maxWidth:480, width:'100%' },
head: { color:'#fff', fontFamily:'monospace', fontSize:18, fontWeight:700, marginBottom:6 }, head: { color:'#fff', fontFamily:'monospace', fontSize:18, fontWeight:700, marginBottom:6 },
sub: { color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:12, marginBottom:24 }, sub: { color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:12, marginBottom:24 },
label: { color:'rgba(255,255,255,0.5)', fontFamily:'monospace', fontSize:11, marginBottom:6, display:'block' },
inp: { width:'100%', boxSizing:'border-box', background:'rgba(255,255,255,0.06)', border:'1px solid rgba(255,255,255,0.12)', borderRadius:8, padding:'10px 14px', color:'#fff', fontFamily:'monospace', fontSize:13, outline:'none' }, inp: { width:'100%', boxSizing:'border-box', background:'rgba(255,255,255,0.06)', border:'1px solid rgba(255,255,255,0.12)', borderRadius:8, padding:'10px 14px', color:'#fff', fontFamily:'monospace', fontSize:13, outline:'none' },
btn: (c='#4ecdc4') => ({ background:`${c}18`, border:`1px solid ${c}44`, borderRadius:8, padding:'10px 16px', color:c, fontFamily:'monospace', fontSize:13, cursor:'pointer', width:'100%', marginTop:10 }), btn: (c='#4ecdc4') => ({ background:`${c}18`, border:`1px solid ${c}44`, borderRadius:8, padding:'10px 16px', color:c, fontFamily:'monospace', fontSize:13, cursor:'pointer', width:'100%', marginTop:10 }),
fileRow: { display:'flex', alignItems:'center', gap:12, padding:'10px 14px', background:'rgba(255,255,255,0.04)', border:'1px solid rgba(255,255,255,0.07)', borderRadius:8, marginBottom:8 }, fileRow: { display:'flex', alignItems:'center', gap:12, padding:'10px 14px', background:'rgba(255,255,255,0.04)', border:'1px solid rgba(255,255,255,0.07)', borderRadius:8, marginBottom:8 },
err: { color:'#f87171', fontFamily:'monospace', fontSize:12, marginTop:8 }, err: { color:'#f87171', fontFamily:'monospace', fontSize:12, marginTop:8 },
}; };
const BASE = '/api/public/file-share/' + token;
useEffect(() => { useEffect(() => {
fetch('/api/public/file-share/' + token) fetch(BASE)
.then(async r => { .then(async r => {
const d = await r.json(); const d = await r.json();
if (!r.ok) { setErrMsg(d.error || 'Link ungültig'); setPhase('error'); return; } if (!r.ok) { setErrMsg(d.error || 'Link ungültig'); setPhase('error'); return; }
setInfo(d); setInfo(d);
setPhase(d.needs_password ? 'pw' : 'ready'); setPhase('pw'); // Passwort immer Pflicht
if (!d.needs_password && d.is_folder) loadFolder('');
}) })
.catch(() => { setErrMsg('Verbindung fehlgeschlagen'); setPhase('error'); }); .catch(() => { setErrMsg('Verbindung fehlgeschlagen'); setPhase('error'); });
}, [token]); }, [token]);
async function loadFolder(pw2) {
const res = await fetch('/api/public/file-share/' + token + '/folder', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ password: pw2 }),
});
const d = await res.json();
if (!res.ok) { setErrMsg(d.error); return; }
setFiles(d.files || []);
setPhase('ready');
}
async function handlePw() { async function handlePw() {
if (!pw.trim()) return; if (!pw.trim()) return;
const res = await fetch('/api/public/file-share/' + token + (info?.is_folder ? '/folder' : '/download'), { const url = BASE + (info?.is_folder ? '/folder' : '/download');
const res = await fetch(url, {
method:'POST', headers:{'Content-Type':'application/json'}, method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ password: pw }), body: JSON.stringify({ password: pw }),
}); });
const d = await res.json(); const d = await res.json();
if (!res.ok) { setErrMsg(d.error); return; } if (!res.ok) { setErrMsg(d.error || 'Falsches Passwort'); return; }
setPassword(pw); setPassword(pw);
setErrMsg(''); setErrMsg('');
if (info?.is_folder) { setFiles(d.files || []); setPhase('ready'); } if (info?.is_folder) { setFiles(d.files || []); setPhase('ready'); }
@@ -2958,16 +2948,17 @@ function PublicFileShare({ token }) {
async function downloadFile(fileId, fileName) { async function downloadFile(fileId, fileName) {
setDlLoading(fileId || 'single'); setDlLoading(fileId || 'single');
try { try {
const res = await fetch( const url = BASE + (fileId ? '/folder/' + fileId : '/download');
'/api/public/file-share/' + token + (fileId ? '/folder/' + fileId : '/download'), const res = await fetch(url, {
{ method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ password }) } method:'POST', headers:{'Content-Type':'application/json'},
); body: JSON.stringify({ password }),
});
if (!res.ok) { const d = await res.json(); setErrMsg(d.error); return; } if (!res.ok) { const d = await res.json(); setErrMsg(d.error); return; }
const blob = await res.blob(); const blob = await res.blob();
const url = URL.createObjectURL(blob); const objUrl = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = fileName; const a = document.createElement('a'); a.href = objUrl; a.download = fileName;
document.body.appendChild(a); a.click(); document.body.removeChild(a); document.body.appendChild(a); a.click(); document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 5000); setTimeout(() => URL.revokeObjectURL(objUrl), 5000);
} finally { setDlLoading(null); } } finally { setDlLoading(null); }
} }
@@ -2978,26 +2969,34 @@ function PublicFileShare({ token }) {
return (b/1024/1024).toFixed(1) + ' MB'; return (b/1024/1024).toFixed(1) + ' MB';
} }
// Abgelaufen/ungültig → Redirect zu google.de
if (phase === 'error') {
window.location.replace('https://www.google.de');
return <div style={S2.wrap}><div style={{color:'#fff',fontFamily:'monospace'}}>Weiterleitung</div></div>;
}
return ( return (
<div style={S2.wrap}> <div style={S2.wrap}>
<div style={S2.box}> <div style={S2.box}>
<div style={S2.head}>🔗 {info?.is_folder ? '📁 Ordner' : '📄 Datei'} Freigabe</div> <div style={S2.head}>🔗 {info?.is_folder ? '📁 Ordner' : '📄 Datei'} Freigabe</div>
<div style={S2.sub}> <div style={S2.sub}>
{info?.folder_name || info?.file_name || info?.label || 'Öffentlicher Link'} {info?.folder_name || info?.file_name || info?.label || 'Öffentlicher Link'}
{info?.expires_at && <span style={{marginLeft:8}}>· Gültig bis {new Date(info.expires_at*1000).toLocaleDateString('de-DE')}</span>} {info?.expires_at && (
<span style={{marginLeft:8}}>
· Gültig bis {new Date(info.expires_at*1000).toLocaleDateString('de-DE')}
</span>
)}
</div> </div>
{phase === 'loading' && <div style={S2.sub}>Lade...</div>} {phase === 'loading' && <div style={S2.sub}>Lade...</div>}
{phase === 'error' && <div style={S2.err}> {errMsg}</div>}
{phase === 'pw' && ( {phase === 'pw' && (
<> <>
<span style={S2.label}>Passwort</span> <span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:11,marginBottom:6,display:'block'}}>Passwort</span>
<input type="password" value={pw} onChange={e=>setPw(e.target.value)} <input value={pw} onChange={e=>setPw(e.target.value)}
onKeyDown={e=>e.key==='Enter'&&handlePw()} autoFocus onKeyDown={e=>e.key==='Enter'&&handlePw()} autoFocus
placeholder="Passwort eingeben" style={S2.inp} /> placeholder="Passwort eingeben" style={S2.inp} />
{errMsg && <div style={S2.err}>{errMsg}</div>} {errMsg && <div style={S2.err}> {errMsg}</div>}
<button onClick={handlePw} style={S2.btn('#4ecdc4')}>🔓 Entsperren</button> <button onClick={handlePw} style={S2.btn('#4ecdc4')}>🔓 Entsperren</button>
</> </>
)} )}
@@ -3011,8 +3010,7 @@ function PublicFileShare({ token }) {
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:11}}>{fmtSize(info.file_size)}</div> <div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:11}}>{fmtSize(info.file_size)}</div>
</div> </div>
</div> </div>
<button onClick={() => downloadFile(null, info.file_name)} <button onClick={() => downloadFile(null, info.file_name)} disabled={dlLoading==='single'}
disabled={dlLoading === 'single'}
style={S2.btn('#4ade80')}> style={S2.btn('#4ade80')}>
{dlLoading==='single' ? '⏳ Lädt...' : '⬇ Datei herunterladen'} {dlLoading==='single' ? '⏳ Lädt...' : '⬇ Datei herunterladen'}
</button> </button>
@@ -3031,8 +3029,7 @@ function PublicFileShare({ token }) {
<div style={{color:'#fff',fontFamily:'monospace',fontSize:12}}>{f.name}</div> <div style={{color:'#fff',fontFamily:'monospace',fontSize:12}}>{f.name}</div>
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>{fmtSize(f.size)}</div> <div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>{fmtSize(f.size)}</div>
</div> </div>
<button onClick={() => downloadFile(f.id, f.name)} <button onClick={()=>downloadFile(f.id, f.name)} disabled={!!dlLoading}
disabled={!!dlLoading}
style={{...S2.btn('#4ade80'), width:'auto', marginTop:0, padding:'6px 14px', fontSize:11}}> style={{...S2.btn('#4ade80'), width:'auto', marginTop:0, padding:'6px 14px', fontSize:11}}>
{dlLoading===f.id ? '⏳' : '⬇'} {dlLoading===f.id ? '⏳' : '⬇'}
</button> </button>
@@ -3046,6 +3043,7 @@ function PublicFileShare({ token }) {
); );
} }
function PublicUpload({ token }) { function PublicUpload({ token }) {
const [phase, setPhase] = useState('loading'); const [phase, setPhase] = useState('loading');
const [info, setInfo] = useState(null); const [info, setInfo] = useState(null);