fix: öffentlicher Share-Endpunkt getrennt, Passwort Pflicht + Generator, kein unbegrenzter Ablauf

This commit is contained in:
2026-06-13 11:38:44 +02:00
parent ac5084e1ee
commit 5d4c6cae17
4 changed files with 151 additions and 24 deletions

View File

@@ -28,16 +28,23 @@ const IconBtn = ({ onClick, color, title, children, stop }) => (
// ── Öffentlicher Link-Share Modal ─────────────────────────────────────────────
function PublicShareModal({ item, itemType, onClose, toast }) {
const [password, setPassword] = useState('');
const [password, setPassword] = useState(() => genPw());
const [showPw, setShowPw] = useState(false);
const [expiresH, setExpiresH] = useState(24);
const [label, setLabel] = useState('');
const [shares, setShares] = useState([]);
const [creating, setCreating] = useState(false);
const [newLink, setNewLink] = useState(null);
const [newPw, setNewPw] = useState('');
const isFolder = itemType === 'folder';
const base = '/tools/dateien/public-shares';
function genPw() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
return Array.from({length:10}, () => chars[Math.floor(Math.random()*chars.length)]).join('');
}
useEffect(() => {
api(base).then(d => setShares((d.shares||[]).filter(s => isFolder
? s.folder_id === item.id
@@ -46,20 +53,23 @@ function PublicShareModal({ item, itemType, onClose, toast }) {
}, []);
async function createShare() {
if (!password.trim()) { toast('Passwort ist Pflicht'); return; }
setCreating(true);
try {
const body = {
[isFolder ? 'folder_id' : 'file_id']: item.id,
password: password || undefined,
expires_hours: expiresH > 0 ? expiresH : undefined,
password: password.trim(),
expires_hours: expiresH,
label: label || undefined,
};
const d = await api(base, { body });
const link = `${window.location.origin}/s/${d.token}`;
setNewLink(link);
setNewPw(password.trim());
setShares(p => [d.share, ...p]);
navigator.clipboard?.writeText(link).catch(()=>{});
toast('Link erstellt & kopiert ✓');
setPassword(genPw());
} catch(e) { toast('Fehler: ' + e.message); }
finally { setCreating(false); }
}
@@ -82,11 +92,10 @@ function PublicShareModal({ item, itemType, onClose, toast }) {
}
const expiryOptions = [
{ label:'1 Stunde', h:1 },
{ label:'1 Stunde', h:1 },
{ label:'24 Stunden', h:24 },
{ label:'7 Tage', h:168 },
{ label:'30 Tage', h:720 },
{ label:'Kein Ablauf', h:0 },
{ label:'7 Tage', h:168 },
{ label:'30 Tage', h:720 },
];
return (
@@ -94,9 +103,7 @@ function PublicShareModal({ item, itemType, onClose, toast }) {
onClick={e=>e.target===e.currentTarget&&onClose()}>
<div style={{background:'#1a1d2e',border:'1px solid rgba(255,255,255,0.1)',borderRadius:14,padding:24,width:'100%',maxWidth:440,maxHeight:'85vh',overflowY:'auto'}}>
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:16}}>
<div style={{color:'#fff',fontFamily:'monospace',fontSize:14,fontWeight:700}}>
🔗 Öffentlicher Link
</div>
<div style={{color:'#fff',fontFamily:'monospace',fontSize:14,fontWeight:700}}>🔗 Öffentlicher Link</div>
<button onClick={onClose} style={{background:'none',border:'none',color:'rgba(255,255,255,0.5)',fontSize:18,cursor:'pointer'}}></button>
</div>
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:11,marginBottom:20}}>
@@ -107,17 +114,32 @@ function PublicShareModal({ item, itemType, onClose, toast }) {
<div style={{marginBottom:20}}>
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10,marginBottom:8,letterSpacing:1}}>NEUEN LINK ERSTELLEN</div>
{/* Label */}
<input value={label} onChange={e=>setLabel(e.target.value)}
placeholder="Bezeichnung (optional)"
style={{...S.inp, width:'100%', boxSizing:'border-box', marginBottom:8}} />
{/* Passwort */}
<input type="password" value={password} onChange={e=>setPassword(e.target.value)}
placeholder="Passwort (optional)"
style={{...S.inp, width:'100%', boxSizing:'border-box', marginBottom:8}} />
{/* Passwort Pflicht + Generator */}
<div style={{display:'flex',gap:6,marginBottom:8}}>
<div style={{flex:1,position:'relative'}}>
<input
type={showPw ? 'text' : 'password'}
value={password}
onChange={e=>setPassword(e.target.value)}
placeholder="Passwort (Pflicht)"
style={{...S.inp, width:'100%', boxSizing:'border-box', paddingRight:36}}
/>
<button onClick={()=>setShowPw(v=>!v)}
style={{position:'absolute',right:8,top:'50%',transform:'translateY(-50%)',background:'none',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:13}}>
{showPw ? '🙈' : '👁'}
</button>
</div>
<button onClick={()=>setPassword(genPw())} title="Passwort generieren"
style={{background:'rgba(245,158,11,0.12)',border:'1px solid rgba(245,158,11,0.3)',borderRadius:7,padding:'0 10px',color:'#f59e0b',cursor:'pointer',fontSize:14,flexShrink:0}}>
🔀
</button>
</div>
{/* Ablauf */}
{/* Ablauf kein "Kein Ablauf" */}
<div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:12}}>
{expiryOptions.map(o => (
<button key={o.h} onClick={()=>setExpiresH(o.h)}
@@ -133,15 +155,23 @@ function PublicShareModal({ item, itemType, onClose, toast }) {
))}
</div>
<button onClick={createShare} disabled={creating}
style={{...S.btn('#4ade80'), width:'100%', opacity:creating?0.5:1}}>
<button onClick={createShare} disabled={creating||!password.trim()}
style={{...S.btn('#4ade80'), width:'100%', opacity:(creating||!password.trim())?0.5:1}}>
{creating ? '⏳ Erstelle...' : '🔗 Link erstellen'}
</button>
{newLink && (
<div style={{marginTop:10, padding:'8px 12px', background:'rgba(74,222,128,0.08)', border:'1px solid rgba(74,222,128,0.3)', borderRadius:8}}>
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:9,marginBottom:4}}>LINK (kopiert)</div>
<div style={{color:'#4ade80',fontFamily:'monospace',fontSize:10,wordBreak:'break-all'}}>{newLink}</div>
<div style={{marginTop:10, padding:'10px 12px', background:'rgba(74,222,128,0.08)', border:'1px solid rgba(74,222,128,0.3)', borderRadius:8}}>
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:9,marginBottom:4}}>LINK (in Zwischenablage)</div>
<div style={{color:'#4ade80',fontFamily:'monospace',fontSize:10,wordBreak:'break-all',marginBottom:6}}>{newLink}</div>
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:9,marginBottom:2}}>PASSWORT</div>
<div style={{display:'flex',alignItems:'center',gap:8}}>
<div style={{color:'#f59e0b',fontFamily:'monospace',fontSize:12,letterSpacing:2}}>{newPw}</div>
<button onClick={()=>navigator.clipboard?.writeText(newPw).then(()=>toast('Passwort kopiert ✓'))}
style={{background:'rgba(245,158,11,0.1)',border:'1px solid rgba(245,158,11,0.3)',borderRadius:5,padding:'2px 8px',color:'#f59e0b',fontFamily:'monospace',fontSize:9,cursor:'pointer'}}>
📋
</button>
</div>
</div>
)}
</div>
@@ -154,8 +184,7 @@ function PublicShareModal({ item, itemType, onClose, toast }) {
<div key={s.id} style={{padding:'10px 12px',background:'rgba(255,255,255,0.04)',border:'1px solid rgba(255,255,255,0.08)',borderRadius:8,marginBottom:8}}>
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:4}}>
<div style={{color:'rgba(255,255,255,0.6)',fontFamily:'monospace',fontSize:11}}>
{s.label || '—'}
{!!s.password_hash && <span style={{marginLeft:6,color:'#f59e0b',fontSize:9}}>🔒</span>}
{s.label || '—'} <span style={{color:'#f59e0b',fontSize:9}}>🔒</span>
</div>
<div style={{display:'flex',gap:6}}>
<button onClick={()=>copyLink(s.token)}