fix: Share-URL-Pfad korrigiert, PW 12 Stellen+Sonderzeichen sichtbar, Standard 1h, aktive Links in Dateiliste

This commit is contained in:
2026-06-13 11:47:24 +02:00
parent 5d4c6cae17
commit d550972848
2 changed files with 81 additions and 16 deletions

View File

@@ -29,8 +29,7 @@ const IconBtn = ({ onClick, color, title, children, stop }) => (
// ── Öffentlicher Link-Share Modal ─────────────────────────────────────────────
function PublicShareModal({ item, itemType, onClose, toast }) {
const [password, setPassword] = useState(() => genPw());
const [showPw, setShowPw] = useState(false);
const [expiresH, setExpiresH] = useState(24);
const [expiresH, setExpiresH] = useState(1);
const [label, setLabel] = useState('');
const [shares, setShares] = useState([]);
const [creating, setCreating] = useState(false);
@@ -41,8 +40,16 @@ function PublicShareModal({ item, itemType, onClose, toast }) {
const base = '/tools/dateien/public-shares';
function genPw() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
return Array.from({length:10}, () => chars[Math.floor(Math.random()*chars.length)]).join('');
const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
const lower = 'abcdefghjkmnpqrstuvwxyz';
const digits = '23456789';
const special = '!@#$%&*-+?';
const all = upper + lower + digits + special;
// Mindestens je 1 aus jeder Kategorie, Rest zufällig
const pick = s => s[Math.floor(Math.random()*s.length)];
const base = [pick(upper), pick(lower), pick(digits), pick(special)];
const rest = Array.from({length:8}, () => pick(all));
return [...base, ...rest].sort(() => Math.random()-0.5).join('');
}
useEffect(() => {
@@ -120,18 +127,14 @@ function PublicShareModal({ item, itemType, onClose, toast }) {
{/* Passwort Pflicht + Generator */}
<div style={{display:'flex',gap:6,marginBottom:8}}>
<div style={{flex:1,position:'relative'}}>
<div style={{flex:1}}>
<input
type={showPw ? 'text' : 'password'}
type="text"
value={password}
onChange={e=>setPassword(e.target.value)}
placeholder="Passwort (Pflicht)"
style={{...S.inp, width:'100%', boxSizing:'border-box', paddingRight:36}}
style={{...S.inp, width:'100%', boxSizing:'border-box', fontFamily:'monospace', letterSpacing:1}}
/>
<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}}>
@@ -589,6 +592,8 @@ export default function Dateien({ toast, mobile }) {
const [search, setSearch] = useState('');
const [shareItem, setShareItem] = useState(null);
const [publicShareItem, setPublicShareItem] = useState(null);
const [publicShares, setPublicShares] = useState([]);
const [showPublicLinks, setShowPublicLinks] = useState(false);
const [pwPrompt, setPwPrompt] = useState(null); // {file}
const [uploading, setUploading] = useState(false);
const [dragOver, setDragOver] = useState(false);
@@ -600,6 +605,7 @@ export default function Dateien({ toast, mobile }) {
const load = useCallback(() => {
setLoading(true);
api('/tools/dateien/public-shares').then(d => setPublicShares(d.shares||[])).catch(()=>{});
const fq = currentFolderId ? `?folder_id=${currentFolderId}` : '';
const pq = currentFolderId ? `?parent_id=${currentFolderId}` : '';
Promise.all([api(`/tools/dateien${fq}`), api(`/tools/dateien/folders${pq}`)])
@@ -726,7 +732,66 @@ export default function Dateien({ toast, mobile }) {
</div>
)}
{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)} toast={toast}/>}
{publicShareItem && <PublicShareModal item={publicShareItem.item} itemType={publicShareItem.type} onClose={()=>{setPublicShareItem(null);load();}} toast={toast}/>}
{/* ── Aktive öffentliche Links ──────────────────────────────────────── */}
{publicShares.length > 0 && tab === 'own' && (
<div style={{marginTop:24,padding:'14px 16px',background:'rgba(255,255,255,0.02)',border:'1px solid rgba(255,255,255,0.07)',borderRadius:10}}>
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:showPublicLinks?12:0,cursor:'pointer'}}
onClick={()=>setShowPublicLinks(v=>!v)}>
<div style={{display:'flex',alignItems:'center',gap:8}}>
<span style={{color:'#4ade80',fontSize:14}}>🔗</span>
<span style={{color:'rgba(255,255,255,0.7)',fontFamily:'monospace',fontSize:12}}>
Aktive öffentliche Links
</span>
<span style={{background:'rgba(74,222,128,0.15)',border:'1px solid rgba(74,222,128,0.3)',borderRadius:10,padding:'1px 7px',color:'#4ade80',fontFamily:'monospace',fontSize:10}}>
{publicShares.length}
</span>
</div>
<span style={{color:'rgba(255,255,255,0.3)',fontSize:11}}>{showPublicLinks ? '▲' : '▼'}</span>
</div>
{showPublicLinks && publicShares.map(s => {
const name = s.file_name || s.folder_name || s.label || '—';
const isExp = s.expires_at && s.expires_at < Math.floor(Date.now()/1000);
const fmtExp = exp => {
if (!exp) return '∞';
const d = new Date(exp*1000);
return d.toLocaleDateString('de-DE')+' '+d.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'});
};
return (
<div key={s.id} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 10px',
background:'rgba(255,255,255,0.03)',border:`1px solid ${isExp?'rgba(248,113,113,0.3)':'rgba(255,255,255,0.07)'}`,
borderRadius:7,marginBottom:6}}>
<span style={{fontSize:14}}>{s.file_id ? '📄' : '📁'}</span>
<div style={{flex:1,minWidth:0}}>
<div style={{color:'rgba(255,255,255,0.7)',fontFamily:'monospace',fontSize:11,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
{name}
{s.label && s.label !== name && <span style={{color:'rgba(255,255,255,0.35)',marginLeft:6}}>({s.label})</span>}
</div>
<div style={{color:isExp?'#f87171':'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9}}>
{isExp?'⚠ Abgelaufen':'Ablauf: '}{!isExp&&fmtExp(s.expires_at)} · {s.download_count} Abrufe · 🔒
</div>
</div>
<button onClick={()=>{
navigator.clipboard?.writeText(window.location.origin+'/s/'+s.token)
.then(()=>toast('Link kopiert ✓')).catch(()=>toast(window.location.origin+'/s/'+s.token));
}}
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 onClick={async()=>{
await api(`/tools/dateien/public-shares/${s.id}`,{method:'DELETE'});
setPublicShares(p=>p.filter(x=>x.id!==s.id));
toast('Link gelöscht');
}}
style={{background:'rgba(248,113,113,0.1)',border:'1px solid rgba(248,113,113,0.3)',borderRadius:5,padding:'4px 8px',color:'#f87171',fontFamily:'monospace',fontSize:9,cursor:'pointer',flexShrink:0}}>
</button>
</div>
);
})}
</div>
)}
{pwPrompt && <PasswordPromptModal
filename={pwPrompt.originalname}
onSubmit={pw=>{ setPwPrompt(null); download(pwPrompt, pw); }}