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:
+18
-7
@@ -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 || '');
|
||||
|
||||
Reference in New Issue
Block a user