Let expired customer links keep accepting uploads
uploadAuth now only blocks archived/missing tokens. A new requireNotExpired middleware sits in front of /files, /file and /zip, so the file browser closes (410 Gone) once the link expires while the upload form stays open. /info reports the expired flag so the page can hide the browser section and show a warning banner. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fd5e917249
commit
b2d6c547a9
|
|
@ -234,8 +234,11 @@ async function init() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
|
window._uploadInfo = data;
|
||||||
document.getElementById('title').textContent = `Upload für ${data.name}`;
|
document.getElementById('title').textContent = `Upload für ${data.name}`;
|
||||||
if (data.expires_at) {
|
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>`;
|
||||||
|
} else if (data.expires_at) {
|
||||||
info.innerHTML = `<span class="expires">⏳ gültig bis ${new Date(data.expires_at).toLocaleString()}</span>`;
|
info.innerHTML = `<span class="expires">⏳ gültig bis ${new Date(data.expires_at).toLocaleString()}</span>`;
|
||||||
} else {
|
} else {
|
||||||
info.textContent = 'Lade Dateien oder ganze Ordner hoch — die Ordnerstruktur bleibt erhalten.';
|
info.textContent = 'Lade Dateien oder ganze Ordner hoch — die Ordnerstruktur bleibt erhalten.';
|
||||||
|
|
@ -269,10 +272,19 @@ function renderCrumbs() {
|
||||||
async function loadFiles() {
|
async function loadFiles() {
|
||||||
const browser = document.getElementById('fileBrowser');
|
const browser = document.getElementById('fileBrowser');
|
||||||
const count = document.getElementById('fileCount');
|
const count = document.getElementById('fileCount');
|
||||||
|
// Browser komplett ausblenden wenn Link abgelaufen ist
|
||||||
|
if (window._uploadInfo && window._uploadInfo.expired) {
|
||||||
|
document.querySelector('.browser').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
renderCrumbs();
|
renderCrumbs();
|
||||||
try {
|
try {
|
||||||
const url = `/u/${token}/files${currentDir ? '?dir=' + encodeURIComponent(currentDir) : ''}`;
|
const url = `/u/${token}/files${currentDir ? '?dir=' + encodeURIComponent(currentDir) : ''}`;
|
||||||
const r = await fetch(url, { headers: authHeaders() });
|
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; }
|
if (!r.ok) { browser.innerHTML = '<div class="empty">Konnte Dateien nicht laden.</div>'; count.textContent = ''; return; }
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
const entries = data.entries || [];
|
const entries = data.entries || [];
|
||||||
|
|
|
||||||
|
|
@ -537,14 +537,15 @@ app.use('/admin/api', api);
|
||||||
// ---------- Customer Upload Portal ----------
|
// ---------- Customer Upload Portal ----------
|
||||||
app.get('/u/:token', (req, res) => {
|
app.get('/u/:token', (req, res) => {
|
||||||
const c = getCustomerByToken(req.params.token);
|
const c = getCustomerByToken(req.params.token);
|
||||||
|
// Expiry no longer blocks the page — only archive/missing token does.
|
||||||
|
// Uploads stay open; the file browser is gated separately below.
|
||||||
if (!c || isArchived(c)) return res.status(404).send('Link nicht gefunden.');
|
if (!c || isArchived(c)) return res.status(404).send('Link nicht gefunden.');
|
||||||
if (isExpired(c)) return res.status(410).send('Link ist abgelaufen.');
|
|
||||||
res.sendFile(path.join(__dirname, '..', 'public', 'upload.html'));
|
res.sendFile(path.join(__dirname, '..', 'public', 'upload.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/u/:token/auth', customerAuthLimiter, async (req, res) => {
|
app.post('/u/:token/auth', customerAuthLimiter, async (req, res) => {
|
||||||
const c = getCustomerByToken(req.params.token);
|
const c = getCustomerByToken(req.params.token);
|
||||||
if (!c || isExpired(c) || isArchived(c)) return res.status(404).json({ error: 'invalid' });
|
if (!c || isArchived(c)) return res.status(404).json({ error: 'invalid' });
|
||||||
if (!c.password_hash) return res.json({ ok: true });
|
if (!c.password_hash) return res.json({ ok: true });
|
||||||
const ok = await bcrypt.compare(req.body.password || '', c.password_hash);
|
const ok = await bcrypt.compare(req.body.password || '', c.password_hash);
|
||||||
res.json({ ok });
|
res.json({ ok });
|
||||||
|
|
@ -552,11 +553,12 @@ app.post('/u/:token/auth', customerAuthLimiter, async (req, res) => {
|
||||||
|
|
||||||
app.get('/u/:token/info', (req, res) => {
|
app.get('/u/:token/info', (req, res) => {
|
||||||
const c = getCustomerByToken(req.params.token);
|
const c = getCustomerByToken(req.params.token);
|
||||||
if (!c || isExpired(c) || isArchived(c)) return res.status(404).json({ error: 'invalid' });
|
if (!c || isArchived(c)) return res.status(404).json({ error: 'invalid' });
|
||||||
res.json({
|
res.json({
|
||||||
name: c.name,
|
name: c.name,
|
||||||
has_password: !!c.password_hash,
|
has_password: !!c.password_hash,
|
||||||
expires_at: c.expires_at,
|
expires_at: c.expires_at,
|
||||||
|
expired: isExpired(c),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -583,9 +585,12 @@ const upload = multer({
|
||||||
limits: { fileSize: 10 * 1024 * 1024 * 1024 },
|
limits: { fileSize: 10 * 1024 * 1024 * 1024 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Customer-side auth: archive/token check + optional password.
|
||||||
|
// Expiry intentionally does NOT block here; uploads stay possible after
|
||||||
|
// the link expired. Use requireNotExpired below to gate read-only actions.
|
||||||
function uploadAuth(req, res, next) {
|
function uploadAuth(req, res, next) {
|
||||||
const c = getCustomerByToken(req.params.token);
|
const c = getCustomerByToken(req.params.token);
|
||||||
if (!c || isExpired(c) || isArchived(c)) return res.status(404).json({ error: 'invalid' });
|
if (!c || isArchived(c)) return res.status(404).json({ error: 'invalid' });
|
||||||
if (c.password_hash) {
|
if (c.password_hash) {
|
||||||
const provided = req.headers['x-upload-password'] || '';
|
const provided = req.headers['x-upload-password'] || '';
|
||||||
bcrypt.compare(provided, c.password_hash).then(ok => {
|
bcrypt.compare(provided, c.password_hash).then(ok => {
|
||||||
|
|
@ -665,7 +670,13 @@ function cdFilename(name) {
|
||||||
return `attachment; filename="${safe}"; filename*=UTF-8''${encoded}`;
|
return `attachment; filename="${safe}"; filename*=UTF-8''${encoded}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get('/u/:token/files', uploadAuth, (req, res) => {
|
// Browse-only guard (after uploadAuth). Uploads are unaffected.
|
||||||
|
function requireNotExpired(req, res, next) {
|
||||||
|
if (isExpired(req._customer)) return res.status(410).json({ error: 'expired' });
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/u/:token/files', uploadAuth, requireNotExpired, (req, res) => {
|
||||||
const c = req._customer;
|
const c = req._customer;
|
||||||
const base = customerDir(c.slug);
|
const base = customerDir(c.slug);
|
||||||
const sub = sanitizeRelPath(req.query.dir || '');
|
const sub = sanitizeRelPath(req.query.dir || '');
|
||||||
|
|
@ -676,7 +687,7 @@ app.get('/u/:token/files', uploadAuth, (req, res) => {
|
||||||
res.json({ dir: sub, entries });
|
res.json({ dir: sub, entries });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/u/:token/file', uploadAuth, (req, res) => {
|
app.get('/u/:token/file', uploadAuth, requireNotExpired, (req, res) => {
|
||||||
const c = req._customer;
|
const c = req._customer;
|
||||||
const base = customerDir(c.slug);
|
const base = customerDir(c.slug);
|
||||||
const rel = sanitizeRelPath(req.query.path || '');
|
const rel = sanitizeRelPath(req.query.path || '');
|
||||||
|
|
@ -699,7 +710,7 @@ app.get('/u/:token/file', uploadAuth, (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stream a ZIP of a folder (or the whole customer dir when dir param is empty).
|
// Stream a ZIP of a folder (or the whole customer dir when dir param is empty).
|
||||||
app.get('/u/:token/zip', uploadAuth, (req, res) => {
|
app.get('/u/:token/zip', uploadAuth, requireNotExpired, (req, res) => {
|
||||||
const c = req._customer;
|
const c = req._customer;
|
||||||
const base = customerDir(c.slug);
|
const base = customerDir(c.slug);
|
||||||
const sub = sanitizeRelPath(req.query.dir || '');
|
const sub = sanitizeRelPath(req.query.dir || '');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue