fix: Share-URL-Pfad korrigiert, PW 12 Stellen+Sonderzeichen sichtbar, Standard 1h, aktive Links in Dateiliste
This commit is contained in:
@@ -2919,7 +2919,7 @@ function PublicFileShare({ token }) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/public/file-share/public/' + token)
|
||||
fetch('/api/public/file-share/' + token)
|
||||
.then(async r => {
|
||||
const d = await r.json();
|
||||
if (!r.ok) { setErrMsg(d.error || 'Link ungültig'); setPhase('error'); return; }
|
||||
@@ -2931,7 +2931,7 @@ function PublicFileShare({ token }) {
|
||||
}, [token]);
|
||||
|
||||
async function loadFolder(pw2) {
|
||||
const res = await fetch('/api/public/file-share/public/' + token + '/folder', {
|
||||
const res = await fetch('/api/public/file-share/' + token + '/folder', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ password: pw2 }),
|
||||
});
|
||||
@@ -2943,7 +2943,7 @@ function PublicFileShare({ token }) {
|
||||
|
||||
async function handlePw() {
|
||||
if (!pw.trim()) return;
|
||||
const res = await fetch('/api/public/file-share/public/' + token + (info?.is_folder ? '/folder' : '/download'), {
|
||||
const res = await fetch('/api/public/file-share/' + token + (info?.is_folder ? '/folder' : '/download'), {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ password: pw }),
|
||||
});
|
||||
@@ -2959,7 +2959,7 @@ function PublicFileShare({ token }) {
|
||||
setDlLoading(fileId || 'single');
|
||||
try {
|
||||
const res = await fetch(
|
||||
'/api/public/file-share/public/' + token + (fileId ? '/folder/' + fileId : '/download'),
|
||||
'/api/public/file-share/' + token + (fileId ? '/folder/' + fileId : '/download'),
|
||||
{ method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ password }) }
|
||||
);
|
||||
if (!res.ok) { const d = await res.json(); setErrMsg(d.error); return; }
|
||||
|
||||
@@ -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); }}
|
||||
|
||||
Reference in New Issue
Block a user