diff --git a/app.rb b/app.rb index 37677eb..19a3568 100644 --- a/app.rb +++ b/app.rb @@ -367,6 +367,7 @@ module Maptool end get "/api/gnss/position" do + require_login! GNSS_LATEST_MUTEX.synchronize do received_at = GNSS_LATEST[:received_at] fresh = received_at && (Time.now.to_f - received_at) < GNSS_STALE_AFTER diff --git a/docs/architecture.adoc b/docs/architecture.adoc index f591418..622be2c 100644 --- a/docs/architecture.adoc +++ b/docs/architecture.adoc @@ -761,6 +761,20 @@ threading a separate flag through, since that's already there and login state can't change without a full page reload anyway. Delete exists on both file and folder rows — both are gated, easy to miss one and only catch it by testing an actually-logged-out session. +`isLoggedIn()` itself lives in `public/js/auth.js` — shared with +`GnssControl` (below), the only other place gating on login. + +`GET /api/gnss/position` requires login too, same reasoning — it +reveals where the receiver physically is, not something to leave open +to anyone with the URL. `GnssControl`'s sidebar icon is hidden +entirely when logged out (`isLoggedIn()`, same pattern as the tree +UI's write controls above) rather than shown and failing to ever get +a fix. `LocateControl` needs no gating of its own here: it only ever +reaches GNSS through `GnssControl#isEnabled()`, which can never turn +true for a logged-out visitor if the toggle that flips it is never on +the page to click — its browser-geolocation fallback stays open to +everyone, since that's a visitor's own device asking for their own +location, nothing to gate. Login delegates to the Gitea instance at `git.priesol.net` via OAuth2 (`GET /login` redirects to Gitea's authorize endpoint, `GET diff --git a/public/js/auth.js b/public/js/auth.js new file mode 100644 index 0000000..102aceb --- /dev/null +++ b/public/js/auth.js @@ -0,0 +1,9 @@ +// No server-rendered flag for this exists, and adding one would be more +// plumbing than reusing what's already on the page: the topbar's own +// logged_in check (views/index.erb) already renders a "Log out" link only +// when logged in. Login state never changes without a full page reload +// (there's no client-side login/logout), so this only needs checking once +// per use, not reactively/on an event. +export function isLoggedIn() { + return !!document.querySelector('a[href="/logout"]'); +} diff --git a/public/js/gnss.js b/public/js/gnss.js index ce1aa65..5dd8201 100644 --- a/public/js/gnss.js +++ b/public/js/gnss.js @@ -1,5 +1,6 @@ import { Marker } from "/vendor/maplibre-gl/maplibre-gl.mjs"; import { GNSS_ICON } from "/js/icons.js"; +import { isLoggedIn } from "/js/auth.js"; const POSITION_URL = "/api/gnss/position"; const POLL_INTERVAL_MS = 1500; @@ -41,6 +42,12 @@ export class GnssControl { this._status = this._container.querySelector(".gnss-control__status"); this._icon.addEventListener("click", () => this._setEnabled(!this._enabled)); + // GET /api/gnss/position itself requires login too (app.rb) — hidden + // here rather than shown and failing on click, same reasoning as + // gpx.js's write-action controls: it reveals where the receiver + // physically is, not something to leave open to anyone with the URL. + if (!isLoggedIn()) this._container.hidden = true; + sidebar.appendChild(this._container); } diff --git a/public/js/gpx.js b/public/js/gpx.js index b29f7bb..d24ebf9 100644 --- a/public/js/gpx.js +++ b/public/js/gpx.js @@ -14,6 +14,7 @@ import { } from "/js/icons.js"; import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js"; import { formatActivityType } from "/js/track_profile.js"; +import { isLoggedIn } from "/js/auth.js"; const TREE_URL = "/api/gpx/tree"; const UPLOAD_URL = "/api/gpx"; @@ -31,17 +32,6 @@ const BADGE_IMAGE_PREFIX = "badge-"; // one registered image per distinct badge // one — both land on the same drop targets, told apart by this. const MOVE_MIME = "application/x-gpx-path"; -// No server-rendered flag for this exists yet, and adding one would be more -// plumbing than reusing what's already on the page: the topbar's own -// logged_in check (views/index.erb) already renders a "Log out" link only -// when logged in. Write-action controls (delete/download/upload/new -// folder) check this to hide themselves entirely rather than being shown -// and failing on click — login state never changes without a full page -// reload, so this only needs checking once per render, not reactively. -function isLoggedIn() { - return !!document.querySelector('a[href="/logout"]'); -} - // A sidebar icon that opens a side panel for uploading GPX files (via // drag & drop, stored server-side in a folder hierarchy the user can add to // and reorganize by dragging), and toggling their tracks/waypoints on and diff --git a/test/test_app.rb b/test/test_app.rb index 798d97a..96b99b6 100644 --- a/test/test_app.rb +++ b/test/test_app.rb @@ -24,10 +24,14 @@ class AppTest < Minitest::Test end def test_gnss_position_unavailable_with_no_fix - response = request.get("/api/gnss/position") + response = request.get("/api/gnss/position", **logged_in_env) assert_equal({ "available" => false }, json_body(response)) end + def test_gnss_position_requires_login + assert_equal 401, request.get("/api/gnss/position").status + end + # --- auth guard --- def test_write_routes_require_login