diff --git a/public/upload.html b/public/upload.html
index a83ec44..3990ede 100644
--- a/public/upload.html
+++ b/public/upload.html
@@ -234,8 +234,11 @@ async function init() {
return;
}
const data = await r.json();
+ window._uploadInfo = data;
document.getElementById('title').textContent = `Upload für ${data.name}`;
- if (data.expires_at) {
+ if (data.expired) {
+ info.innerHTML = `⚠ Link ist abgelaufen — Uploads sind weiterhin möglich, der Datei-Browser ist deaktiviert.`;
+ } else if (data.expires_at) {
info.innerHTML = `⏳ gültig bis ${new Date(data.expires_at).toLocaleString()}`;
} else {
info.textContent = 'Lade Dateien oder ganze Ordner hoch — die Ordnerstruktur bleibt erhalten.';
@@ -269,10 +272,19 @@ function renderCrumbs() {
async function loadFiles() {
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';
+ 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 = '
Konnte Dateien nicht laden.
'; count.textContent = ''; return; }
const data = await r.json();
const entries = data.entries || [];
diff --git a/src/server.js b/src/server.js
index e024379..e7e9b4d 100644
--- a/src/server.js
+++ b/src/server.js
@@ -537,14 +537,15 @@ app.use('/admin/api', api);
// ---------- Customer Upload Portal ----------
app.get('/u/:token', (req, res) => {
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 (isExpired(c)) return res.status(410).send('Link ist abgelaufen.');
res.sendFile(path.join(__dirname, '..', 'public', 'upload.html'));
});
app.post('/u/:token/auth', customerAuthLimiter, async (req, res) => {
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 });
const ok = await bcrypt.compare(req.body.password || '', c.password_hash);
res.json({ ok });
@@ -552,11 +553,12 @@ app.post('/u/:token/auth', customerAuthLimiter, async (req, res) => {
app.get('/u/:token/info', (req, res) => {
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({
name: c.name,
has_password: !!c.password_hash,
expires_at: c.expires_at,
+ expired: isExpired(c),
});
});
@@ -583,9 +585,12 @@ const upload = multer({
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) {
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) {
const provided = req.headers['x-upload-password'] || '';
bcrypt.compare(provided, c.password_hash).then(ok => {
@@ -665,7 +670,13 @@ function cdFilename(name) {
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 base = customerDir(c.slug);
const sub = sanitizeRelPath(req.query.dir || '');
@@ -676,7 +687,7 @@ app.get('/u/:token/files', uploadAuth, (req, res) => {
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 base = customerDir(c.slug);
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).
-app.get('/u/:token/zip', uploadAuth, (req, res) => {
+app.get('/u/:token/zip', uploadAuth, requireNotExpired, (req, res) => {
const c = req._customer;
const base = customerDir(c.slug);
const sub = sanitizeRelPath(req.query.dir || '');