feat: PW-Kopier-Button in Link-Liste, Bezeichnung ohne PW sichtbar, kein Auto-Download
This commit is contained in:
@@ -2898,7 +2898,7 @@ function AdminPanel({ toast, mobile, user, nav }) {
|
|||||||
|
|
||||||
// ── Öffentliche Datei/Ordner-Download-Seite ────────────────────────────────
|
// ── Öffentliche Datei/Ordner-Download-Seite ────────────────────────────────
|
||||||
function PublicFileShare({ token }) {
|
function PublicFileShare({ token }) {
|
||||||
const [phase, setPhase] = useState('loading');
|
const [phase, setPhase] = useState('loading'); // loading|pw|ready|error
|
||||||
const [info, setInfo] = useState(null);
|
const [info, setInfo] = useState(null);
|
||||||
const [files, setFiles] = useState([]);
|
const [files, setFiles] = useState([]);
|
||||||
const [pw, setPw] = useState('');
|
const [pw, setPw] = useState('');
|
||||||
@@ -2923,19 +2923,24 @@ function PublicFileShare({ token }) {
|
|||||||
fetch(BASE)
|
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) { setPhase('error'); return; }
|
||||||
setInfo(d);
|
setInfo(d);
|
||||||
setPhase('pw'); // Passwort immer Pflicht
|
setPhase('pw');
|
||||||
})
|
})
|
||||||
.catch(() => { setErrMsg('Verbindung fehlgeschlagen'); setPhase('error'); });
|
.catch(() => setPhase('error'));
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
|
// Sofort redirect ohne Inhalt anzeigen
|
||||||
|
if (phase === 'error') {
|
||||||
|
window.location.replace('https://www.google.de');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
async function handlePw() {
|
async function handlePw() {
|
||||||
if (!pw.trim()) return;
|
if (!pw.trim()) return;
|
||||||
setErrMsg('');
|
setErrMsg('');
|
||||||
|
|
||||||
if (info?.is_folder) {
|
if (info?.is_folder) {
|
||||||
// Ordner: JSON-Response mit Dateiliste
|
|
||||||
const res = await fetch(BASE + '/folder', {
|
const res = await fetch(BASE + '/folder', {
|
||||||
method:'POST', headers:{'Content-Type':'application/json'},
|
method:'POST', headers:{'Content-Type':'application/json'},
|
||||||
body: JSON.stringify({ password: pw }),
|
body: JSON.stringify({ password: pw }),
|
||||||
@@ -2946,8 +2951,7 @@ function PublicFileShare({ token }) {
|
|||||||
setFiles(d.files || []);
|
setFiles(d.files || []);
|
||||||
setPhase('ready');
|
setPhase('ready');
|
||||||
} else {
|
} else {
|
||||||
// Datei: erst Passwort verifizieren, dann Phase wechseln
|
// Datei: erst PW testen via Download (streams bei Erfolg)
|
||||||
// Download-Endpoint streamt direkt → wir prüfen Content-Type
|
|
||||||
const res = await fetch(BASE + '/download', {
|
const res = await fetch(BASE + '/download', {
|
||||||
method:'POST', headers:{'Content-Type':'application/json'},
|
method:'POST', headers:{'Content-Type':'application/json'},
|
||||||
body: JSON.stringify({ password: pw }),
|
body: JSON.stringify({ password: pw }),
|
||||||
@@ -2957,15 +2961,14 @@ function PublicFileShare({ token }) {
|
|||||||
setErrMsg(d.error || 'Falsches Passwort');
|
setErrMsg(d.error || 'Falsches Passwort');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Passwort korrekt → direkt Download starten
|
// Korrekt → Download starten + Phase wechseln
|
||||||
setPassword(pw);
|
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
const objUrl = URL.createObjectURL(blob);
|
const objUrl = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = objUrl;
|
a.href = objUrl; a.download = info.file_name || 'download';
|
||||||
a.download = info.file_name || 'download';
|
|
||||||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||||||
setTimeout(() => URL.revokeObjectURL(objUrl), 5000);
|
setTimeout(() => URL.revokeObjectURL(objUrl), 5000);
|
||||||
|
setPassword(pw);
|
||||||
setPhase('ready');
|
setPhase('ready');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2994,30 +2997,34 @@ function PublicFileShare({ token }) {
|
|||||||
return (b/1024/1024).toFixed(1) + ' MB';
|
return (b/1024/1024).toFixed(1) + ' MB';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abgelaufen/ungültig → sofort zu google.de
|
// Ablauf formatieren
|
||||||
if (phase === 'error') {
|
const fmtExp = exp => exp
|
||||||
window.location.replace('https://www.google.de');
|
? new Date(exp*1000).toLocaleDateString('de-DE') + ' ' + new Date(exp*1000).toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'}) + ' Uhr'
|
||||||
return null;
|
: null;
|
||||||
}
|
|
||||||
|
|
||||||
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.sub}>
|
|
||||||
{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', {day:'2-digit',month:'2-digit',year:'numeric'})} {new Date(info.expires_at*1000).toLocaleTimeString('de-DE', {hour:'2-digit',minute:'2-digit'})} Uhr
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{phase === 'loading' && <div style={S2.sub}>Lade...</div>}
|
{/* Immer sichtbar: Bezeichnung + Ablauf */}
|
||||||
|
<div style={S2.head}>🔗 Freigabe</div>
|
||||||
|
{info?.label && (
|
||||||
|
<div style={{color:'rgba(255,255,255,0.7)',fontFamily:'monospace',fontSize:14,marginBottom:4}}>
|
||||||
|
{info.label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{info?.expires_at && (
|
||||||
|
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10,marginBottom:20}}>
|
||||||
|
Gültig bis {fmtExp(info.expires_at)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PW-Eingabe */}
|
||||||
{phase === 'pw' && (
|
{phase === 'pw' && (
|
||||||
<>
|
<>
|
||||||
<span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:11,marginBottom:6,display:'block'}}>Passwort</span>
|
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:11,marginBottom:6}}>
|
||||||
|
🔒 Passwort erforderlich
|
||||||
|
</div>
|
||||||
<input 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} />
|
||||||
@@ -3026,6 +3033,7 @@ function PublicFileShare({ token }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Datei bereit */}
|
||||||
{phase === 'ready' && !info?.is_folder && (
|
{phase === 'ready' && !info?.is_folder && (
|
||||||
<>
|
<>
|
||||||
<div style={{...S2.fileRow, marginBottom:16}}>
|
<div style={{...S2.fileRow, marginBottom:16}}>
|
||||||
@@ -3035,20 +3043,18 @@ 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>
|
||||||
<div style={{color:'rgba(74,222,128,0.8)',fontFamily:'monospace',fontSize:12,marginBottom:10,textAlign:'center'}}>
|
|
||||||
✓ Download gestartet
|
|
||||||
</div>
|
|
||||||
<button onClick={() => downloadFile(null, info.file_name)} disabled={dlLoading==='single'}
|
<button onClick={() => downloadFile(null, info.file_name)} disabled={dlLoading==='single'}
|
||||||
style={S2.btn('#4ade80')}>
|
style={S2.btn('#4ade80')}>
|
||||||
{dlLoading==='single' ? '⏳ Lädt...' : '⬇ Erneut herunterladen'}
|
{dlLoading==='single' ? '⏳ Lädt...' : '⬇ Datei herunterladen'}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Ordner bereit */}
|
||||||
{phase === 'ready' && info?.is_folder && (
|
{phase === 'ready' && info?.is_folder && (
|
||||||
<>
|
<>
|
||||||
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:11,marginBottom:12}}>
|
<div style={{color:'rgba(255,255,255,0.6)',fontFamily:'monospace',fontSize:12,marginBottom:12}}>
|
||||||
{files.length} Datei{files.length!==1?'en':''} im Ordner
|
📁 {info.folder_name} · {files.length} Datei{files.length!==1?'en':''}
|
||||||
</div>
|
</div>
|
||||||
{files.map(f => (
|
{files.map(f => (
|
||||||
<div key={f.id} style={S2.fileRow}>
|
<div key={f.id} style={S2.fileRow}>
|
||||||
@@ -3066,6 +3072,10 @@ function PublicFileShare({ token }) {
|
|||||||
{files.length===0 && <div style={S2.sub}>Ordner ist leer.</div>}
|
{files.length===0 && <div style={S2.sub}>Ordner ist leer.</div>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{phase === 'loading' && (
|
||||||
|
<div style={S2.sub}>Lade...</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const IconBtn = ({ onClick, color, title, children, stop }) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ── Öffentlicher Link-Share Modal ─────────────────────────────────────────────
|
// ── Öffentlicher Link-Share Modal ─────────────────────────────────────────────
|
||||||
function PublicShareModal({ item, itemType, onClose, toast }) {
|
function PublicShareModal({ item, itemType, onClose, onShareCreated, toast }) {
|
||||||
const [password, setPassword] = useState(() => genPw());
|
const [password, setPassword] = useState(() => genPw());
|
||||||
const [expiresH, setExpiresH] = useState(1);
|
const [expiresH, setExpiresH] = useState(1);
|
||||||
const [label, setLabel] = useState('');
|
const [label, setLabel] = useState('');
|
||||||
@@ -74,6 +74,7 @@ function PublicShareModal({ item, itemType, onClose, toast }) {
|
|||||||
setNewLink(link);
|
setNewLink(link);
|
||||||
setNewPw(password.trim());
|
setNewPw(password.trim());
|
||||||
setShares(p => [d.share, ...p]);
|
setShares(p => [d.share, ...p]);
|
||||||
|
onShareCreated?.(d.token, password.trim());
|
||||||
navigator.clipboard?.writeText(link).catch(()=>{});
|
navigator.clipboard?.writeText(link).catch(()=>{});
|
||||||
toast('Link erstellt & kopiert ✓');
|
toast('Link erstellt & kopiert ✓');
|
||||||
setPassword(genPw());
|
setPassword(genPw());
|
||||||
@@ -594,6 +595,7 @@ export default function Dateien({ toast, mobile }) {
|
|||||||
const [publicShareItem, setPublicShareItem] = useState(null);
|
const [publicShareItem, setPublicShareItem] = useState(null);
|
||||||
const [publicShares, setPublicShares] = useState([]);
|
const [publicShares, setPublicShares] = useState([]);
|
||||||
const [showPublicLinks, setShowPublicLinks] = useState(false);
|
const [showPublicLinks, setShowPublicLinks] = useState(false);
|
||||||
|
const [sharePws, setSharePws] = useState({}); // {token: plaintext_pw}
|
||||||
const [pwPrompt, setPwPrompt] = useState(null); // {file}
|
const [pwPrompt, setPwPrompt] = useState(null); // {file}
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [dragOver, setDragOver] = useState(false);
|
const [dragOver, setDragOver] = useState(false);
|
||||||
@@ -732,7 +734,7 @@ export default function Dateien({ toast, mobile }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{shareItem && <ShareModal item={shareItem.item} type={shareItem.type} onClose={()=>{setShareItem(null);load();}} toast={toast}/>}
|
{shareItem && <ShareModal item={shareItem.item} type={shareItem.type} onClose={()=>{setShareItem(null);load();}} toast={toast}/>}
|
||||||
{publicShareItem && <PublicShareModal item={publicShareItem.item} itemType={publicShareItem.type} onClose={()=>{setPublicShareItem(null);api('/tools/dateien/public-shares').then(d=>setPublicShares(d.shares||[])).catch(()=>{});}} toast={toast}/>}
|
{publicShareItem && <PublicShareModal item={publicShareItem.item} itemType={publicShareItem.type} onClose={()=>{setPublicShareItem(null);api('/tools/dateien/public-shares').then(d=>setPublicShares(d.shares||[])).catch(()=>{});}} onShareCreated={(token,pw)=>setSharePws(p=>({...p,[token]:pw}))} toast={toast}/>}
|
||||||
|
|
||||||
{/* ── Aktive öffentliche Links ──────────────────────────────────────── */}
|
{/* ── Aktive öffentliche Links ──────────────────────────────────────── */}
|
||||||
{publicShares.length > 0 && tab === 'own' && (
|
{publicShares.length > 0 && tab === 'own' && (
|
||||||
@@ -779,6 +781,16 @@ export default function Dateien({ toast, mobile }) {
|
|||||||
style={{background:'rgba(74,222,128,0.1)',border:'1px solid rgba(74,222,128,0.3)',borderRadius:5,padding:'4px 8px',color:'#4ade80',fontFamily:'monospace',fontSize:9,cursor:'pointer',flexShrink:0}}>
|
style={{background:'rgba(74,222,128,0.1)',border:'1px solid rgba(74,222,128,0.3)',borderRadius:5,padding:'4px 8px',color:'#4ade80',fontFamily:'monospace',fontSize:9,cursor:'pointer',flexShrink:0}}>
|
||||||
📋
|
📋
|
||||||
</button>
|
</button>
|
||||||
|
{sharePws[s.token] && (
|
||||||
|
<button onClick={()=>{
|
||||||
|
navigator.clipboard?.writeText(sharePws[s.token])
|
||||||
|
.then(()=>toast('Passwort kopiert ✓')).catch(()=>toast(sharePws[s.token]));
|
||||||
|
}}
|
||||||
|
style={{background:'rgba(245,158,11,0.1)',border:'1px solid rgba(245,158,11,0.3)',borderRadius:5,padding:'4px 8px',color:'#f59e0b',fontFamily:'monospace',fontSize:9,cursor:'pointer',flexShrink:0}}
|
||||||
|
title={sharePws[s.token]}>
|
||||||
|
🔑
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button onClick={async()=>{
|
<button onClick={async()=>{
|
||||||
await api(`/tools/dateien/public-shares/${s.id}`,{method:'DELETE'});
|
await api(`/tools/dateien/public-shares/${s.id}`,{method:'DELETE'});
|
||||||
setPublicShares(p=>p.filter(x=>x.id!==s.id));
|
setPublicShares(p=>p.filter(x=>x.id!==s.id));
|
||||||
|
|||||||
Reference in New Issue
Block a user