From c1a000c28539757c90e9f2d4f66a6ec316f419b7 Mon Sep 17 00:00:00 2001 From: malice Date: Fri, 22 May 2026 16:40:49 -0600 Subject: [PATCH] Gate Traffic toggle on auth.authenticated (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: /api/traffic is on Caddy's @authed_api, so when logged out MapLibre's raster tile fetches receive a 302 to the Authentik login (HTML), which it can't decode as an image and retries on every map move — console spam and a stuck-feeling Traffic toggle. Fix (frontend-only; /api/traffic stays auth-gated in Caddy): - LayerControl: the Traffic toggle is always rendered but disabled (greyed, "Sign in to enable traffic" tooltip) until auth has loaded AND the user is authenticated — mirroring Panel.jsx's contacts gating. The add-traffic apply effect now also requires auth.authenticated (and lists it in deps), and the mount init only restores saved traffic=true when authenticated. - Teardown on session -> anonymous: an effect flips traffic:false once auth has loaded and the user is not authenticated, which drives the apply effect to removeTrafficLayer (no further tile requests). - MapView: the style-reload re-apply (which re-adds layers from localStorage on theme/style changes) now also checks auth.authenticated for traffic, so it can't re-add the source for an anonymous session — the second add path that would otherwise reintroduce the 302 retry loop. - localStorage hydration: LayerControl now subscribes via useConfig() and its init effect depends on [config] instead of [], so saved layer prefs hydrate correctly once /api/config resolves (previously, mounting before config loaded left toggles stuck off and never re-initialized). Shown-but-disabled (not hidden) so logged-in users see no flicker on reload during the brief pre-whoami window. Tests: the navi repo has no test infrastructure (no vitest/jest); bootstrapping is out of scope. Follow-up: seed a vitest + RTL test asserting the Traffic toggle is disabled when !auth.authenticated. Co-authored-by: Matt Johnson Co-authored-by: Claude Opus 4.7 (1M context) --- src/components/LayerControl.jsx | 32 ++++++++++++++++++++++++++------ src/components/MapView.jsx | 4 +++- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/components/LayerControl.jsx b/src/components/LayerControl.jsx index ee41ccb..31dcbc2 100644 --- a/src/components/LayerControl.jsx +++ b/src/components/LayerControl.jsx @@ -1,6 +1,7 @@ import { useState, useEffect, useRef } from 'react' import { Layers, Map, Satellite, Globe } from 'lucide-react' import { hasFeature, getConfig } from '../config' +import { useConfig } from '../hooks/useConfig' import { useStore } from '../store' const STORAGE_KEY = 'navi-layer-prefs' @@ -33,7 +34,14 @@ export default function LayerControl({ mapRef }) { const viewMode = useStore((s) => s.viewMode) const setViewMode = useStore((s) => s.setViewMode) - // Initialize from localStorage or defaults on mount + // Auth state — Traffic tiles are auth-gated at the edge (Caddy @authed_api), + // so the toggle is only usable when authenticated. config drives re-init once + // /api/config resolves (so saved prefs hydrate against known feature flags). + const auth = useStore((s) => s.auth) + const config = useConfig() + const trafficDisabled = !auth.loaded || !auth.authenticated + + // Initialize from localStorage or defaults on mount (re-runs when config loads) useEffect(() => { const saved = loadPrefs() const hsAvailable = hasFeature('has_hillshade') @@ -47,7 +55,7 @@ export default function LayerControl({ mapRef }) { if (saved) { setHillshade(hsAvailable && (saved.hillshade ?? true)) - setTraffic(trAvailable && (saved.traffic ?? false)) + setTraffic(trAvailable && auth.authenticated && (saved.traffic ?? false)) setPublicLands(plAvailable && (saved.publicLands ?? false)) setContours(ctAvailable && (saved.contours ?? false)) setContoursTest(ctTestAvailable && (saved.contoursTest ?? false)) @@ -64,7 +72,14 @@ export default function LayerControl({ mapRef }) { setContoursTest10ft(false) setUsfsTrails(false) } - }, []) + }, [config]) + + // Tear down traffic when the session goes anonymous (only after auth has + // loaded, so we don't tear down during the brief pre-whoami window on reload). + // Flipping the pref off drives the apply effect below -> removeTrafficLayer. + useEffect(() => { + if (auth.loaded && !auth.authenticated && traffic) setTraffic(false) + }, [auth.loaded, auth.authenticated]) // eslint-disable-line react-hooks/exhaustive-deps // Apply layers when prefs change useEffect(() => { @@ -97,7 +112,7 @@ export default function LayerControl({ mapRef }) { if (!map) return const apply = () => { - if (traffic && hasFeature('has_traffic_overlay')) { + if (traffic && hasFeature('has_traffic_overlay') && auth.authenticated) { mapView.addTrafficLayer?.() } else { mapView.removeTrafficLayer?.() @@ -111,7 +126,7 @@ export default function LayerControl({ mapRef }) { } savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails }) return () => map.off('style.load', apply) - }, [traffic, mapRef]) + }, [traffic, mapRef, auth.authenticated]) useEffect(() => { const mapView = mapRef?.current @@ -343,12 +358,17 @@ export default function LayerControl({ mapRef }) { )} {showTraffic && ( -