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/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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user