From 0045d41a7469931fdadee8c736c6cc5842045690 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 3 Jul 2026 02:08:10 +0200 Subject: [PATCH] fix(downloads): file download honours the per-tab account (?account=) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A plain file download can't send the X-Siftlode-Account header, so current_user resolved it to the session-default account — 404 'Unknown download' when the tab acts as a non-default wallet account that owns the file. resolved_user_id now also honours a ?account= query param (the same wallet-gated selection the WebSocket already uses), and downloadFileUrl appends the active account id. Verified: default account -> 404, ?account= -> 206 with the right Content-Disposition. --- backend/app/auth.py | 5 ++++- frontend/src/lib/api.ts | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/app/auth.py b/backend/app/auth.py index c654dca..f7f6041 100644 --- a/backend/app/auth.py +++ b/backend/app/auth.py @@ -683,7 +683,10 @@ def resolved_user_id(request: Request) -> tuple[int | None, bool]: """ default_id = request.session.get("user_id") wallet = request.session.get("account_ids") or [] - hdr = request.headers.get(ACTIVE_ACCOUNT_HEADER) + # Header for normal XHR; ?account= for contexts that can't set headers (WebSocket, and a + # plain file download). Both are wallet-gated below, so neither can impersonate an + # account that never signed into this browser. + hdr = request.headers.get(ACTIVE_ACCOUNT_HEADER) or request.query_params.get("account") if hdr: try: hid = int(hdr) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 031be80..5c5d8a1 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1044,7 +1044,13 @@ export const api = { req(`/api/downloads/${id}/share`, { method: "POST", body: JSON.stringify({ email }) }), unshareDownload: (id: number, email: string) => req(`/api/downloads/${id}/share/${encodeURIComponent(email)}`, { method: "DELETE" }), - downloadFileUrl: (id: number): string => `/api/downloads/${id}/file`, + // A plain navigation can't send the X-Siftlode-Account header, so pass the active + // account via ?account= (same wallet-gated selection the WebSocket uses) — otherwise the + // download resolves to the session-default account and 404s for a per-tab account's file. + downloadFileUrl: (id: number): string => { + const a = getActiveAccount(); + return a != null ? `/api/downloads/${id}/file?account=${a}` : `/api/downloads/${id}/file`; + }, // admin adminDownloads: (): Promise => req("/api/admin/downloads"),