Mint the download token when the button is pressed

A friend got "The requested resource wasn't found." downloading the adult
Windows installer. It is not a permissions problem and not missing data:
the row and the file are both there, and the same download returns 206 with
a valid token. That message is exactly what a protected file answers when
the token is missing or expired, verified against production.

releases.file is protected, so the URL needs a file token, and a file token
lives 180 seconds -- read off the JWT's own exp claim, not guessed. The
download list minted three of them while building the rows and baked them
into the hrefs, so three minutes after the page loaded every button was
permanently dead, and pressing it again could not help because the stale
token was in the markup. Nothing suggested reloading.

The owner never saw it because he presses the button as soon as the tab
renders. The setup flow I added this week makes it near-certain for anyone
else: it walks somebody through choosing services, creating accounts,
paying and fetching a TMDB key before pointing them at Stáhnout, which is
many minutes after the download tab was first rendered.

So the token is minted in the click handler now, and the link carries
download=1 so PocketBase sends an attachment rather than leaving the
browser to decide what to do with a .exe or a .zip.

The app was already correct: UpdateService mints its token immediately
before _dio.download, so auto-update on the family's devices was never
affected. This was the web page only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-08-12 22:18:33 +02:00
parent 874eab21dc
commit dd37b36f6e

View file

@ -1201,22 +1201,45 @@ async function loadDownloads(){
li.style.cssText="padding:12px;border:1px solid var(--line);border-radius:10px;"+
"margin-bottom:8px;background:var(--sunk);font-size:14px;list-style:none";
if (m.ok && m.data && m.data.available){
// `releases.file` is a protected file: the path alone 403s, it needs a
// short-lived file token. Minted per link, exactly as the landing page does.
var t=await api("POST","/api/files/token",null);
var href=(t.ok && t.data && t.data.token)
? m.data.downloadPath+"?token="+encodeURIComponent(t.data.token) : null;
// **The token is minted when the button is pressed, not when this list is
// built.** `releases.file` is protected, so the path needs a file token, and a
// file token lives 180 seconds — measured, not assumed. Baking one into the
// href at render time meant every download button died three minutes after
// the page loaded, permanently, until someone thought to reload. PocketBase
// answers an expired token with 404 "The requested resource wasn't found.",
// which reads like the file is gone rather than like "press it again".
//
// That is not a hypothetical: a friend hit it on the Windows build. The setup
// flow makes it near-certain, because it walks somebody through buying
// subscriptions and fetching a TMDB key before it points them here, which is
// many minutes, and the download tab may have been rendered at sign-in.
li.innerHTML='<div class="rowbtn"><div><b>'+esc(p[1])+'</b><div class="meta">'+
esc(m.data.version||"")+" · "+esc(m.data.variant||"")+'</div></div>'+
(href ? '<a class="btn primary" style="width:auto;padding:9px 14px;margin:0;'+
'text-decoration:none;display:inline-block" href="'+href+'">Stáhnout</a>'
: '<span class="meta">odkaz se nepodařilo vytvořit</span>')+'</div>';
'<button class="btn primary" style="width:auto;padding:9px 14px;margin:0" '+
'data-dl="'+esc(m.data.downloadPath)+'">Stáhnout</button></div>';
any=true;
} else {
li.innerHTML="<b>"+esc(p[1])+'</b><div class="meta">zatím nic ke stažení</div>';
}
box.appendChild(li);
}
// Wired after the rows exist, so each press mints its own token. `download=1`
// makes PocketBase send the file as an attachment instead of leaving the browser
// to decide what to do with a .zip or an .exe.
Array.prototype.forEach.call(box.querySelectorAll("button[data-dl]"), function(b){
b.onclick=async function(){
busy(b,true,"Připravuji…");
var t=await api("POST","/api/files/token",null);
busy(b,false);
if(!(t.ok && t.data && t.data.token)){
setMsg($("dlMsg"),"Odkaz se nepodařilo vytvořit. Zkus to prosím znovu.","err");
return;
}
setMsg($("dlMsg"),"");
window.location.href = b.getAttribute("data-dl") +
"?token=" + encodeURIComponent(t.data.token) + "&download=1";
};
});
if (!any) setMsg($("dlMsg"),"Zatím není publikovaná žádná verze.","");
}