From eca851915251937e36b7389cd3f2d0acb36fe664 Mon Sep 17 00:00:00 2001 From: Roman Priesol Date: Mon, 7 Sep 2026 23:18:59 +0200 Subject: [PATCH] Require login to export files; allow downloading the original too ?download=... on GET /api/gpx/file now requires login, same as upload/move/delete/create-folder -- plain viewing (no ?download) stays public. Added ?download=original alongside the existing GeoJSON export, serving the untouched, as-uploaded file directly via send_file -- the file name link still downloads GeoJSON, a new button next to delete downloads the original. Since both are real links (so right-click/middle-click still work), a logged-out click can't just be left as a plain navigation -- it would land on a raw JSON error page instead of being sent to log in. _download (gpx.js) fetches instead, reuses the existing redirectIfUnauthorized 401 check, and only then turns the response into a blob and clicks a throwaway to trigger the save. Verified in a real browser: an anonymous click on either download correctly redirects through /login to the real OAuth login page instead of showing raw JSON. --- app.rb | 26 ++++++++++++++--- docs/architecture.adoc | 40 ++++++++++++++++++--------- public/js/gpx.js | 63 ++++++++++++++++++++++++++++++++++-------- test/test_app.rb | 24 ++++++++++++++-- 4 files changed, 123 insertions(+), 30 deletions(-) diff --git a/app.rb b/app.rb index 928f0d6..b7d14b7 100644 --- a/app.rb +++ b/app.rb @@ -191,11 +191,29 @@ module Maptool json paths: paths, tree: GPX_STORE.tree end - # ?download=1 exports the file's GeoJSON as an attachment instead of - # rendering it on the map — the only way to get a .gpx/.fit's converted - # form out of the app, since GpxStore#geojson's cache is otherwise an - # internal implementation detail with no filename of its own to expose. + # ?download=geojson exports the file's GeoJSON as an attachment instead + # of rendering it on the map — the only way to get a .gpx/.fit's + # converted form out of the app, since GpxStore#geojson's cache is + # otherwise an internal implementation detail with no filename of its + # own to expose. ?download=original exports the untouched, as-uploaded + # file instead (a .geojson source's "original" is that same file, so + # the two downloads are identical there — harmless, not special-cased). + # Both gated the same as the other write/manage actions below (upload, + # move, create folder) — viewing a file on the map stays open to + # anyone, exporting it as a file does not. get "/api/gpx/file/*" do + require_login! if params[:download] + + if params[:download] == "original" + begin + path = GPX_STORE.resolve_file(params[:splat].first) + rescue GpxStore::InvalidPath + halt 400 + end + halt 404 unless File.file?(path) + return send_file(path, filename: File.basename(path), disposition: "attachment") + end + begin body = GPX_STORE.geojson(params[:splat].first) rescue GpxStore::InvalidPath diff --git a/docs/architecture.adoc b/docs/architecture.adoc index 361bc60..27f33f6 100644 --- a/docs/architecture.adoc +++ b/docs/architecture.adoc @@ -343,18 +343,29 @@ name instead — `"ride.gpx"` reads as `"ride.gpx.geojson"`, matching the real cache file sitting next to it on disk — since the GeoJSON, not the original, is what's actually rendered on the map from that point on. The original is never deleted or re-parsed once cached, just -not re-shown as itself. Two ways to get that converted form out of the -app as an actual file (`GET /api/gpx/file/?download=1`) — the -cache is otherwise an implementation detail with no name of its own to -expose: the file name itself is a real link (an ``, not a ``, -specifically so clicking it downloads instead of also toggling the -row's checkbox — a native ``/`