Fix Upload-Seite: als React-Komponente statt externer HTML, SW-Probleme beseitigt
This commit is contained in:
@@ -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/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');
|
||||
|
||||
app.get('/manifest.json', (_req, res) => {
|
||||
@@ -85,13 +91,6 @@ app.get('/sw.js', (_req, res) => {
|
||||
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, {
|
||||
setHeaders: (res, filePath) => {
|
||||
// JS/CSS Assets haben Hash im Namen → lang cachen
|
||||
|
||||
@@ -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 { TOOLS, getGroupedTools } from './toolRegistry.js';
|
||||
import CalendarWidget, { CalendarSettings } from './calendar.jsx';
|
||||
@@ -2850,7 +2850,125 @@ function AdminPanel({ toast, mobile, user, nav }) {
|
||||
|
||||
|
||||
// ── 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() {
|
||||
// Ö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 [splash, setSplash] = useState(() => {
|
||||
// Only show splash in standalone PWA mode, and only once per session
|
||||
|
||||
Reference in New Issue
Block a user