diff --git a/backend/src/index.js b/backend/src/index.js index 9541f51..c16df2f 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -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 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 479332b..d30d4f8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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 ( +
DickenDock Upload
+ + {phase==='loading' &&