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
+13 -2
View File
@@ -64,6 +64,15 @@ const customerAuthLimiter = rateLimit({
legacyHeaders: false,
message: { error: 'too many attempts, try again later' },
});
// Polling endpoints — generous but not unlimited, mainly to cap abuse from
// leaked tokens spamming the cheap read endpoints.
const customerPollLimiter = rateLimit({
windowMs: 60 * 1000,
max: 60,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'rate limited' },
});
// ---------- Helpers ----------
function slugify(name) {
@@ -551,9 +560,10 @@ app.post('/u/:token/auth', customerAuthLimiter, async (req, res) => {
res.json({ ok });
});
app.get('/u/:token/info', (req, res) => {
app.get('/u/:token/info', customerPollLimiter, (req, res) => {
const c = getCustomerByToken(req.params.token);
if (!c || isArchived(c)) return res.status(404).json({ error: 'invalid' });
res.setHeader('Cache-Control', 'no-store');
res.json({
name: c.name,
has_password: !!c.password_hash,
@@ -676,7 +686,7 @@ function requireNotExpired(req, res, next) {
next();
}
app.get('/u/:token/files', uploadAuth, requireNotExpired, (req, res) => {
app.get('/u/:token/files', customerPollLimiter, uploadAuth, requireNotExpired, (req, res) => {
const c = req._customer;
const base = customerDir(c.slug);
const sub = sanitizeRelPath(req.query.dir || '');
@@ -684,6 +694,7 @@ app.get('/u/:token/files', uploadAuth, requireNotExpired, (req, res) => {
try { entries = listCustomerDir(base, sub); }
catch { return res.status(400).json({ error: 'invalid path' }); }
if (entries === null) return res.status(404).json({ error: 'not found' });
res.setHeader('Cache-Control', 'no-store');
res.json({ dir: sub, entries });
});