Upload-Share: Route registriert, Fehlversuche-Limit, PW kopieren, bessere Fehlerbehandlung

This commit is contained in:
2026-06-05 23:25:45 +02:00
parent 7e19a6580e
commit e7ebf2d831
5 changed files with 187 additions and 189 deletions

View File

@@ -7,140 +7,181 @@
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#0d0d0f;color:#fff;font-family:'Courier New',monospace;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
.card{background:#1a1a1e;border:1px solid rgba(255,255,255,0.1);border-radius:16px;padding:32px;width:100%;max-width:440px}
.card{background:#1a1a1e;border:1px solid rgba(255,255,255,0.1);border-radius:16px;padding:28px;width:100%;max-width:440px}
h1{font-size:18px;margin-bottom:6px;color:#4ecdc4}
.sub{color:rgba(255,255,255,0.4);font-size:12px;margin-bottom:24px}
label{display:block;color:rgba(255,255,255,0.5);font-size:10px;letter-spacing:2px;margin-bottom:4px}
input[type=password],input[type=file]{width:100%;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.12);border-radius:8px;padding:10px 12px;color:#fff;font-family:inherit;font-size:14px;outline:none}
.sub{color:rgba(255,255,255,0.4);font-size:12px;margin-bottom:20px}
label{display:block;color:rgba(255,255,255,0.5);font-size:10px;letter-spacing:2px;margin-bottom:4px;margin-top:12px}
input[type=password],input[type=file]{width:100%;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.12);border-radius:8px;padding:10px 12px;color:#fff;font-family:inherit;font-size:14px;outline:none;transition:border 0.2s}
input[type=password]:focus{border-color:#4ecdc4}
input[type=file]{cursor:pointer;padding:8px}
input[type=file]::file-selector-button{background:#4ecdc4;border:none;border-radius:6px;color:#0d0d0f;cursor:pointer;font-family:inherit;font-weight:700;padding:4px 12px;margin-right:10px}
.btn{width:100%;background:#4ecdc4;border:none;border-radius:8px;color:#0d0d0f;cursor:pointer;font-family:inherit;font-size:14px;font-weight:700;padding:12px;margin-top:12px}
.btn{width:100%;background:#4ecdc4;border:none;border-radius:8px;color:#0d0d0f;cursor:pointer;font-family:inherit;font-size:14px;font-weight:700;padding:12px;margin-top:12px;transition:opacity 0.2s}
.btn:hover:not(:disabled){opacity:0.85}
.btn:disabled{opacity:0.4;cursor:default}
.btn.danger{background:#ff6b9d}
.info{background:rgba(78,205,196,0.08);border:1px solid rgba(78,205,196,0.2);border-radius:8px;padding:10px 12px;font-size:12px;color:rgba(255,255,255,0.6);margin-bottom:16px}
.msg{border-radius:8px;padding:10px 12px;font-size:13px;margin-top:12px}
.info{background:rgba(78,205,196,0.08);border:1px solid rgba(78,205,196,0.2);border-radius:8px;padding:10px 12px;font-size:12px;color:rgba(255,255,255,0.6);margin-bottom:16px;margin-top:4px}
.msg{border-radius:8px;padding:10px 12px;font-size:13px;margin-top:10px}
.msg.ok{background:rgba(78,205,196,0.12);border:1px solid rgba(78,205,196,0.3);color:#4ecdc4}
.msg.err{background:rgba(255,107,157,0.12);border:1px solid rgba(255,107,157,0.3);color:#ff6b9d}
.msg.warn{background:rgba(255,230,109,0.12);border:1px solid rgba(255,230,109,0.3);color:#ffe66d}
.hidden{display:none}
.done-list{margin-top:12px;font-size:12px;color:rgba(255,255,255,0.5)}
.done-list li{padding:3px 0;list-style:none}
.done-list{margin-top:10px;font-size:12px;color:rgba(255,255,255,0.5)}
.done-list li{padding:2px 0;list-style:none}
.done-list li::before{content:'✓ ';color:#4ecdc4}
#progress{height:4px;background:rgba(78,205,196,0.2);border-radius:2px;margin-top:8px;overflow:hidden}
#progress{height:4px;background:rgba(78,205,196,0.15);border-radius:2px;margin-top:10px;overflow:hidden}
#progress-bar{height:100%;background:#4ecdc4;width:0;transition:width 0.3s}
.attempts{color:#ffe66d;font-size:11px;margin-top:6px;font-family:monospace}
</style>
</head>
<body>
<div class="card">
<h1>📤 Datei-Upload</h1>
<p class="sub" id="sub">Dateien hochladen</p>
<h1>📤 Datei hochladen</h1>
<p class="sub" id="sub">Dateien senden</p>
<!-- Passwort-Sektion -->
<div id="pw-section">
<div class="info" id="share-info">Link wird geprüft…</div>
<div class="info" id="share-info">Link wird geprüft…</div>
<label>PASSWORT</label>
<input type="password" id="pw" placeholder="Passwort eingeben" autocomplete="off"/>
<input type="password" id="pw" placeholder="Passwort eingeben" autocomplete="off" onkeydown="if(event.key==='Enter')verify()"/>
<button class="btn" id="pw-btn" onclick="verify()">Zugang bestätigen</button>
<div id="pw-msg" class="msg hidden"></div>
<div id="pw-msg" class="hidden"></div>
<div id="attempts-info" class="attempts hidden"></div>
</div>
<!-- Upload-Sektion -->
<div id="upload-section" class="hidden">
<div class="info" id="upload-info"></div>
<label>DATEI AUSWÄHLEN</label>
<label>DATEI(EN) AUSWÄHLEN</label>
<input type="file" id="file-input" multiple/>
<div id="progress"><div id="progress-bar"></div></div>
<button class="btn" id="upload-btn" onclick="upload()">⬆ Hochladen</button>
<div id="upload-msg" class="msg hidden"></div>
<button class="btn" id="upload-btn" onclick="uploadFiles()">⬆ Hochladen</button>
<div id="upload-msg" class="hidden"></div>
<ul class="done-list" id="done-list"></ul>
</div>
</div>
<script>
const token = location.pathname.replace('/u/','').replace('/','');
const token = location.pathname.split('/u/')[1] || '';
let password = '';
let maxMb = 10;
async function init() {
if (!token) return showFatal('Ungültiger Link-Aufbau');
try {
const r = await fetch(`/api/upload-shares/public/${token}`);
const r = await fetch('/api/upload-shares/public/' + encodeURIComponent(token));
const d = await r.json();
if (!r.ok) return showFatal(d.error || 'Ungültiger Link');
if (!r.ok) return showFatal(d.error || 'Link ungültig');
maxMb = d.max_size_mb;
const exp = new Date(d.expires_at).toLocaleString('de-DE', {timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
document.getElementById('share-info').textContent = `Gültig bis: ${exp} · Max ${d.max_size_mb} MB pro Datei`;
} catch { showFatal('Verbindung fehlgeschlagen'); }
const exp = new Date(d.expires_at).toLocaleString('de-DE',{timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
document.getElementById('share-info').textContent = '🔐 Gültig bis ' + exp + ' · max ' + d.max_size_mb + ' MB pro Datei';
} catch(e) {
showFatal('Verbindung fehlgeschlagen: ' + e.message);
}
}
function showFatal(msg) {
document.getElementById('pw-section').innerHTML = `<div class="msg err">${msg}</div>`;
document.getElementById('pw-section').innerHTML = '<div class="msg err">' + msg + '</div>';
}
async function verify() {
const pw = document.getElementById('pw').value;
if (!pw) return;
document.getElementById('pw-btn').disabled = true;
if (!pw) { document.getElementById('pw').focus(); return; }
const btn = document.getElementById('pw-btn');
btn.disabled = true;
btn.textContent = '…';
const msg = document.getElementById('pw-msg');
try {
const r = await fetch(`/api/upload-shares/public/${token}/verify`, {
method:'POST', headers:{'Content-Type':'application/json'},
const r = await fetch('/api/upload-shares/public/' + encodeURIComponent(token) + '/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pw })
});
const d = await r.json();
if (!r.ok) {
document.getElementById('pw-msg').className = 'msg err';
document.getElementById('pw-msg').textContent = d.error;
document.getElementById('pw-msg').classList.remove('hidden');
document.getElementById('pw-btn').disabled = false;
msg.className = 'msg err';
msg.textContent = d.error;
msg.classList.remove('hidden');
if (d.locked) {
showFatal(d.error);
return;
}
if (d.attempts_left !== undefined) {
const ai = document.getElementById('attempts-info');
ai.textContent = '⚠ Noch ' + d.attempts_left + ' Versuch' + (d.attempts_left===1?'':'e') + ' übrig';
ai.classList.remove('hidden');
}
btn.disabled = false;
btn.textContent = 'Zugang bestätigen';
return;
}
password = pw;
maxMb = d.max_size_mb;
document.getElementById('pw-section').classList.add('hidden');
document.getElementById('upload-section').classList.remove('hidden');
const exp = new Date(d.expires_at).toLocaleString('de-DE', {timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
document.getElementById('upload-info').textContent = `Gültig bis ${exp} · Max ${d.max_size_mb} MB pro Datei`;
document.getElementById('sub').textContent = 'Dateien hochladen ✓';
} catch {
document.getElementById('pw-btn').disabled = false;
const us = document.getElementById('upload-section');
us.classList.remove('hidden');
const exp = new Date(d.expires_at).toLocaleString('de-DE',{timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
document.getElementById('upload-info').textContent = '✓ Zugang bestätigt · Gültig bis ' + exp + ' · max ' + d.max_size_mb + ' MB pro Datei';
document.getElementById('sub').textContent = 'Bereit zum Hochladen';
} catch(e) {
msg.className = 'msg err';
msg.textContent = 'Verbindung fehlgeschlagen: ' + e.message;
msg.classList.remove('hidden');
btn.disabled = false;
btn.textContent = 'Zugang bestätigen';
}
}
async function upload() {
async function uploadFiles() {
const files = document.getElementById('file-input').files;
if (!files.length) return;
if (!files.length) { document.getElementById('file-input').click(); return; }
const btn = document.getElementById('upload-btn');
btn.disabled = true;
const msg = document.getElementById('upload-msg');
const bar = document.getElementById('progress-bar');
const msg = document.getElementById('upload-msg');
const bar = document.getElementById('progress-bar');
const list = document.getElementById('done-list');
msg.classList.add('hidden');
for (let i=0; i<files.length; i++) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.size > maxMb*1024*1024) {
msg.className = 'msg err'; msg.textContent = `"${file.name}" ist zu groß (max ${maxMb} MB)`; msg.classList.remove('hidden');
if (file.size > maxMb * 1024 * 1024) {
msg.className = 'msg warn';
msg.textContent = '"' + file.name + '" ist zu groß (max ' + maxMb + ' MB)';
msg.classList.remove('hidden');
continue;
}
bar.style.width = ((i+0.5)/files.length*100)+'%';
const fd = new FormData(); fd.append('file', file);
bar.style.width = ((i + 0.5) / files.length * 100) + '%';
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 r = await fetch('/api/upload-shares/public/' + encodeURIComponent(token) + '/upload', {
method: 'POST',
headers: { 'x-upload-password': password },
body: fd
});
const d = await r.json();
if (r.ok) {
const li = document.createElement('li'); li.textContent = file.name; list.appendChild(li);
msg.className = 'msg ok'; msg.textContent = `${list.children.length} Datei(en) erfolgreich hochgeladen`; msg.classList.remove('hidden');
const li = document.createElement('li');
li.textContent = file.name;
list.appendChild(li);
} else {
msg.className = 'msg err'; msg.textContent = d.error; msg.classList.remove('hidden');
msg.className = 'msg err';
msg.textContent = d.error;
msg.classList.remove('hidden');
}
} catch { msg.className='msg err'; msg.textContent='Upload fehlgeschlagen'; msg.classList.remove('hidden'); }
} catch(e) {
msg.className = 'msg err';
msg.textContent = 'Upload fehlgeschlagen: ' + e.message;
msg.classList.remove('hidden');
}
}
bar.style.width = '100%';
setTimeout(()=>{ bar.style.width='0'; }, 1500);
setTimeout(() => bar.style.width = '0', 1500);
if (list.children.length > 0) {
msg.className = 'msg ok';
msg.textContent = list.children.length + ' Datei' + (list.children.length !== 1 ? 'en' : '') + ' erfolgreich hochgeladen';
msg.classList.remove('hidden');
}
btn.disabled = false;
document.getElementById('file-input').value = '';
}
document.getElementById('pw').addEventListener('keydown', e => { if(e.key==='Enter') verify(); });
init();
</script>
</body>