Fix Upload-Seite: als React-Komponente statt externer HTML, SW-Probleme beseitigt

This commit is contained in:
2026-06-06 00:46:40 +02:00
parent e7ebf2d831
commit f50a9840f7
2 changed files with 125 additions and 8 deletions

View File

@@ -71,6 +71,12 @@ app.use('/api/tools/qrcodes', require('./tools/qrcodes/routes'));
app.use('/api/upload-shares', require('./tools/dateien/upload-share')); app.use('/api/upload-shares', require('./tools/dateien/upload-share'));
app.use('/api/search', require('./routes/search')); app.use('/api/search', require('./routes/search'));
// Öffentliche Upload-Seite: React-App ausliefern App erkennt /u/ Pfad selbst
app.get('/u/:token', (_req, res) => {
res.setHeader('Cache-Control', 'no-store, no-cache');
res.sendFile(require('path').join(PUBLIC, 'index.html'));
});
const PUBLIC = path.join(__dirname, '../public'); const PUBLIC = path.join(__dirname, '../public');
app.get('/manifest.json', (_req, res) => { app.get('/manifest.json', (_req, res) => {
@@ -85,13 +91,6 @@ app.get('/sw.js', (_req, res) => {
res.sendFile(path.join(PUBLIC, 'sw.js')); res.sendFile(path.join(PUBLIC, 'sw.js'));
}); });
// Öffentliche Upload-Seite (kein Login erforderlich)
const UPLOAD_HTML = path.join(__dirname, '../upload-page.html');
app.get('/u/:token', (_req, res) => {
if (require('fs').existsSync(UPLOAD_HTML)) res.sendFile(UPLOAD_HTML);
else res.status(404).send('Upload-Seite nicht gefunden');
});
app.use(express.static(PUBLIC, { app.use(express.static(PUBLIC, {
setHeaders: (res, filePath) => { setHeaders: (res, filePath) => {
// JS/CSS Assets haben Hash im Namen → lang cachen // JS/CSS Assets haben Hash im Namen → lang cachen

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback, useRef, useCallback } from 'react';
import { api, S } from './lib.js'; import { api, S } from './lib.js';
import { TOOLS, getGroupedTools } from './toolRegistry.js'; import { TOOLS, getGroupedTools } from './toolRegistry.js';
import CalendarWidget, { CalendarSettings } from './calendar.jsx'; import CalendarWidget, { CalendarSettings } from './calendar.jsx';
@@ -2850,7 +2850,125 @@ function AdminPanel({ toast, mobile, user, nav }) {
// ── App Shell ───────────────────────────────────────────────────────────────── // ── App Shell ─────────────────────────────────────────────────────────────────
// ── Öffentliche Upload-Seite ─────────────────────────────────────────────────
function PublicUpload({ token }) {
const [phase, setPhase] = useState('loading');
const [info, setInfo] = useState(null);
const [errorMsg, setErrorMsg] = useState('');
const [pw, setPw] = useState('');
const [password, setPassword] = useState('');
const [uploading,setUploading]= useState(false);
const [done, setDone] = useState([]);
const [msg, setMsg] = useState(null);
const [attLeft, setAttLeft] = useState(null);
const fileRef = useRef(null);
useEffect(() => {
fetch('/api/upload-shares/public/' + token)
.then(async r => {
const d = await r.json();
if (!r.ok) { setErrorMsg(d.error||'Link ungültig'); setPhase('error'); return; }
setInfo(d); setPhase('pw');
})
.catch(e => { setErrorMsg('Verbindung fehlgeschlagen: '+e.message); setPhase('error'); });
}, [token]);
const verify = async () => {
if (!pw.trim()) return;
try {
const r = await fetch('/api/upload-shares/public/'+token+'/verify', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ password: pw })
});
const d = await r.json();
if (!r.ok) {
setMsg({text:d.error,type:'error'});
if (d.locked) { setPhase('error'); setErrorMsg(d.error); return; }
if (d.attempts_left!=null) setAttLeft(d.attempts_left);
return;
}
setPassword(pw); setInfo(d); setPhase('upload'); setMsg(null);
} catch(e) { setMsg({text:'Netzwerkfehler: '+e.message,type:'error'}); }
};
const doUpload = async () => {
const files = fileRef.current?.files;
if (!files?.length) { fileRef.current?.click(); return; }
setUploading(true); setMsg(null);
const newDone = [];
for (const file of files) {
if (file.size > info.max_size_mb * 1024 * 1024) {
setMsg({text:`"${file.name}" zu groß (max ${info.max_size_mb} MB)`,type:'warn'}); continue;
}
const fd = new FormData(); fd.append('file', file);
try {
const r = await fetch('/api/upload-shares/public/'+token+'/upload', {
method:'POST', headers:{'x-upload-password': password}, body:fd
});
const d = await r.json();
if (r.ok) newDone.push(file.name);
else setMsg({text:d.error,type:'error'});
} catch(e) { setMsg({text:e.message,type:'error'}); }
}
if (newDone.length) {
setDone(p=>[...p,...newDone]);
setMsg({text:newDone.length+' Datei'+(newDone.length!==1?'en':'')+' hochgeladen ✓',type:'ok'});
}
if (fileRef.current) fileRef.current.value='';
setUploading(false);
};
const fmtDate = d => new Date(d).toLocaleString('de-DE',{timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
const C = {
page: {minHeight:'100vh',background:'#0d0d0f',display:'flex',alignItems:'center',justifyContent:'center',padding:20},
card: {background:'#1a1a1e',border:'1px solid rgba(255,255,255,0.1)',borderRadius:16,padding:28,width:'100%',maxWidth:440},
inp: {width:'100%',background:'rgba(255,255,255,0.06)',border:'1px solid rgba(255,255,255,0.12)',borderRadius:8,padding:'10px 12px',color:'#fff',fontFamily:"'Courier New',monospace",fontSize:14,outline:'none',boxSizing:'border-box'},
infoBox: {background:'rgba(78,205,196,0.08)',border:'1px solid rgba(78,205,196,0.2)',borderRadius:8,padding:'10px 12px',fontSize:12,color:'rgba(255,255,255,0.6)',marginBottom:14},
btn: {width:'100%',background:'#4ecdc4',border:'none',borderRadius:8,color:'#0d0d0f',cursor:'pointer',fontFamily:"'Courier New',monospace",fontSize:14,fontWeight:700,padding:12,marginTop:12},
msg: t=>({borderRadius:8,padding:'10px 12px',fontSize:13,marginTop:10,...(t==='ok'?{background:'rgba(78,205,196,0.12)',border:'1px solid rgba(78,205,196,0.3)',color:'#4ecdc4'}:t==='warn'?{background:'rgba(255,230,109,0.12)',border:'1px solid rgba(255,230,109,0.3)',color:'#ffe66d'}:{background:'rgba(255,107,157,0.12)',border:'1px solid rgba(255,107,157,0.3)',color:'#ff6b9d'})}),
};
return (
<div style={{...C.page,fontFamily:"'Courier New',monospace"}}>
<div style={C.card}>
<h1 style={{fontSize:18,color:'#4ecdc4',margin:'0 0 4px'}}>📤 Datei hochladen</h1>
<p style={{color:'rgba(255,255,255,0.35)',fontSize:12,marginBottom:18}}>DickenDock Upload</p>
{phase==='loading' && <div style={C.infoBox}> Link wird geprüft</div>}
{phase==='error' && <div style={C.msg('error')}>{errorMsg}</div>}
{phase==='pw' && info && <>
<div style={C.infoBox}>🔐 Gültig bis {fmtDate(info.expires_at)} · max {info.max_size_mb} MB pro Datei</div>
<div style={{color:'rgba(255,255,255,0.45)',fontSize:10,letterSpacing:2,marginBottom:4}}>PASSWORT</div>
<input type="password" value={pw} onChange={e=>setPw(e.target.value)}
onKeyDown={e=>e.key==='Enter'&&verify()} placeholder="Passwort eingeben"
autoCapitalize="none" autoComplete="off" style={C.inp}/>
{attLeft!=null && <div style={{color:'#ffe66d',fontSize:11,marginTop:6}}> Noch {attLeft} Versuch{attLeft===1?'':'e'} übrig</div>}
<button onClick={verify} style={C.btn}>Zugang bestätigen</button>
{msg && <div style={C.msg(msg.type)}>{msg.text}</div>}
</>}
{phase==='upload' && info && <>
<div style={C.infoBox}> Zugang OK · max {info.max_size_mb} MB · bis {fmtDate(info.expires_at)}</div>
<input type="file" ref={fileRef} multiple style={{...C.inp,padding:8,cursor:'pointer'}}/>
<button onClick={doUpload} disabled={uploading} style={{...C.btn,opacity:uploading?0.5:1}}>
{uploading?'⏳ Lädt hoch…':'⬆ Hochladen'}
</button>
{msg && <div style={C.msg(msg.type)}>{msg.text}</div>}
{done.map((f,i)=><div key={i} style={{color:'rgba(78,205,196,0.7)',fontSize:11,padding:'2px 0'}}> {f}</div>)}
</>}
</div>
</div>
);
}
export default function App() { export default function App() {
// Öffentliche Upload-Seite kein Login, kein Auth
const _uploadMatch = window.location.pathname.match(/^\/u\/(.+)$/);
if (_uploadMatch) return <PublicUpload token={_uploadMatch[1]}/>;
const mobile = useIsMobile(); const mobile = useIsMobile();
const [splash, setSplash] = useState(() => { const [splash, setSplash] = useState(() => {
// Only show splash in standalone PWA mode, and only once per session // Only show splash in standalone PWA mode, and only once per session