Add auto-refresh on the customer page and harden the new poll endpoints

The upload page now polls /info and /files every 20s while visible:
new uploads (also via WebDAV), expiry changes and link deactivation
appear without a manual reload. A pollBusy flag prevents overlapping
fetches on slow connections, and visibilitychange stops the timer in
backgrounded tabs. /info and /files get Cache-Control: no-store so the
browser cannot serve stale state, plus a 60/min/IP customerPollLimiter
to cap abuse from leaked tokens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-16 15:36:07 +02:00
parent b2d6c547a9
commit 82782c4f92
2 changed files with 117 additions and 35 deletions
+104 -33
View File
@@ -225,28 +225,97 @@ async function applyBranding() {
} catch {}
}
async function init() {
await applyBranding();
const r = await fetch(`/u/${token}/info`);
if (!r.ok) {
document.querySelector('.card').innerHTML =
'<h1>Link ungültig oder abgelaufen</h1><p class="subtitle">Bitte wende dich an deinen Ansprechpartner für einen neuen Link.</p>';
return;
}
const data = await r.json();
function applyInfo(data) {
window._uploadInfo = data;
document.getElementById('title').textContent = `Upload für ${data.name}`;
if (data.expired) {
info.innerHTML = `<span class="expires" style="color:var(--warn,#f59e0b); border-color:var(--warn,#f59e0b)">⚠ Link ist abgelaufen — Uploads sind weiterhin möglich, der Datei-Browser ist deaktiviert.</span>`;
info.innerHTML = `<span class="expires" style="color:#f59e0b; border-color:#f59e0b">⚠ Link ist abgelaufen — Uploads sind weiterhin möglich, der Datei-Browser ist deaktiviert.</span>`;
} else if (data.expires_at) {
info.innerHTML = `<span class="expires">⏳ gültig bis ${new Date(data.expires_at).toLocaleString()}</span>`;
} else {
info.textContent = 'Lade Dateien oder ganze Ordner hoch — die Ordnerstruktur bleibt erhalten.';
}
// Browser visibility synced to expiry
const browser = document.querySelector('.browser');
if (browser) browser.style.display = data.expired ? 'none' : '';
}
function showLinkGone(reason) {
stopPolling();
document.querySelector('.card').innerHTML =
`<h1>Link nicht mehr verfügbar</h1><p class="subtitle">${reason}</p>`;
}
async function init() {
await applyBranding();
const r = await fetch(`/u/${token}/info`);
if (!r.ok) {
showLinkGone('Bitte wende dich an deinen Ansprechpartner für einen neuen Link.');
return;
}
const data = await r.json();
applyInfo(data);
if (data.has_password) gate.style.display = 'block';
else { main.style.display = 'block'; loadFiles(); }
startPolling();
}
// --- Auto-refresh ---
const POLL_INTERVAL_MS = 20000;
let pollTimer = null;
let pollBusy = false;
let lastInfoSig = '';
let lastFilesSig = '';
async function pollState() {
if (pollBusy) return;
pollBusy = true;
try {
const r = await fetch(`/u/${token}/info`);
if (r.status === 404) {
showLinkGone('Der Upload-Link wurde deaktiviert oder gelöscht.');
return;
}
if (!r.ok) return;
const data = await r.json();
const sig = JSON.stringify(data);
if (sig !== lastInfoSig) {
lastInfoSig = sig;
applyInfo(data);
// Newly expired or unexpired → reload list
if (!data.expired && main.style.display !== 'none') loadFiles();
}
if (!data.expired && main.style.display !== 'none') await refreshFilesIfChanged();
} catch {}
finally { pollBusy = false; }
}
async function refreshFilesIfChanged() {
try {
const url = `/u/${token}/files${currentDir ? '?dir=' + encodeURIComponent(currentDir) : ''}`;
const r = await fetch(url, { headers: authHeaders() });
if (r.status === 410) { applyInfo({ ...(window._uploadInfo || {}), expired: true }); return; }
if (!r.ok) return;
const data = await r.json();
const sig = JSON.stringify(data);
if (sig === lastFilesSig) return;
lastFilesSig = sig;
renderFiles(data);
} catch {}
}
function startPolling() {
if (pollTimer) return;
pollTimer = setInterval(pollState, POLL_INTERVAL_MS);
}
function stopPolling() {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
}
document.addEventListener('visibilitychange', () => {
if (document.hidden) stopPolling();
else if (window._uploadInfo) startPolling();
});
function authHeaders() {
return password ? { 'X-Upload-Password': password } : {};
}
@@ -269,31 +338,16 @@ function renderCrumbs() {
cr.innerHTML = html;
}
async function loadFiles() {
function renderFiles(data) {
const browser = document.getElementById('fileBrowser');
const count = document.getElementById('fileCount');
// Browser komplett ausblenden wenn Link abgelaufen ist
if (window._uploadInfo && window._uploadInfo.expired) {
document.querySelector('.browser').style.display = 'none';
const entries = (data && data.entries) || [];
count.textContent = entries.length ? `(${entries.length})` : '';
if (!entries.length) {
browser.innerHTML = '<div class="empty">Dieser Ordner ist leer.</div>';
return;
}
renderCrumbs();
try {
const url = `/u/${token}/files${currentDir ? '?dir=' + encodeURIComponent(currentDir) : ''}`;
const r = await fetch(url, { headers: authHeaders() });
if (r.status === 410) {
document.querySelector('.browser').style.display = 'none';
return;
}
if (!r.ok) { browser.innerHTML = '<div class="empty">Konnte Dateien nicht laden.</div>'; count.textContent = ''; return; }
const data = await r.json();
const entries = data.entries || [];
count.textContent = entries.length ? `(${entries.length})` : '';
if (!entries.length) {
browser.innerHTML = '<div class="empty">Dieser Ordner ist leer.</div>';
return;
}
browser.innerHTML = entries.map(e => {
browser.innerHTML = entries.map(e => {
const name = esc(e.name);
const date = new Date(e.mtime).toLocaleString();
const fullPath = currentDir ? currentDir + '/' + e.name : e.name;
@@ -321,7 +375,24 @@ async function loadFiles() {
<button type="button" class="btn dl" data-path="${escPath}">⬇ Download</button>
</div>`;
}).join('');
} catch (ex) {
}
async function loadFiles() {
const browser = document.getElementById('fileBrowser');
if (window._uploadInfo && window._uploadInfo.expired) {
document.querySelector('.browser').style.display = 'none';
return;
}
renderCrumbs();
try {
const url = `/u/${token}/files${currentDir ? '?dir=' + encodeURIComponent(currentDir) : ''}`;
const r = await fetch(url, { headers: authHeaders() });
if (r.status === 410) { document.querySelector('.browser').style.display = 'none'; return; }
if (!r.ok) { browser.innerHTML = '<div class="empty">Konnte Dateien nicht laden.</div>'; return; }
const data = await r.json();
lastFilesSig = JSON.stringify(data);
renderFiles(data);
} catch {
browser.innerHTML = '<div class="empty">Fehler beim Laden.</div>';
}
}
@@ -382,7 +453,7 @@ document.getElementById('pwBtn').onclick = async () => {
body: JSON.stringify({ password: pw }),
});
const j = await r.json();
if (j.ok) { password = pw; gate.style.display='none'; main.style.display='block'; loadFiles(); }
if (j.ok) { password = pw; gate.style.display='none'; main.style.display='block'; loadFiles(); startPolling(); }
else document.getElementById('pwErr').style.display='block';
};