mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-09-18 07:02:20 +02:00
fix(basemap): serve CARTO key from backend, bound the map gate, add source attribution
Review follow-up: - Drop the Next.js route. CARTO_API_KEY is now a regular backend registry key (env, .env, or the API Keys panel) served by public GET /api/basemap-config. Every frontend mode already proxies /api/* to the backend (Next.js proxy in web mode, companion server in packaged desktop), so this covers web and desktop with one mechanism and leaves the static export untouched. Also removes the invalid non-handler export from the route module by removing the module. - useBasemapConfig: fail open to the unkeyed style after 3 s, abort the request at 15 s, apply a late key when it arrives, cache successes per page and retry failures on the next mount. - Declare OSM/CARTO attribution on the raster source (same markup as the viewer's existing AttributionControl so MapLibre de-duplicates it). - Tests: backend endpoint (unset / set+trimmed / persisted operator key / registry), hook behaviour (success, non-OK, network error, soft timeout then late key, hard abort, shared request and retry), attribution and gating source checks. - CARTO_API_KEY moves to the backend service in docker-compose.yml; docs updated accordingly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
71550b4adf
commit
667f51cb7a
+7
-1
@@ -9072,7 +9072,7 @@ async def api_sentinel_tile(request: Request):
|
||||
# ---------------------------------------------------------------------------
|
||||
# API Settings — key registry & management
|
||||
# ---------------------------------------------------------------------------
|
||||
from services.api_settings import get_api_keys, get_env_path_info
|
||||
from services.api_settings import get_api_keys, get_basemap_config, get_env_path_info
|
||||
from services.shodan_connector import (
|
||||
ShodanConnectorError,
|
||||
count_shodan,
|
||||
@@ -9111,6 +9111,12 @@ async def api_get_keys_meta(request: Request):
|
||||
return get_env_path_info()
|
||||
|
||||
|
||||
@app.get("/api/basemap-config")
|
||||
@limiter.limit("60/minute")
|
||||
async def api_basemap_config(request: Request):
|
||||
return get_basemap_config()
|
||||
|
||||
|
||||
@app.get("/api/tools/shodan/status", dependencies=[Depends(require_local_operator)])
|
||||
@limiter.limit("30/minute")
|
||||
async def api_shodan_status(request: Request):
|
||||
|
||||
@@ -225,6 +225,15 @@ API_REGISTRY = [
|
||||
"url": "https://dataspace.copernicus.eu/",
|
||||
"required": False,
|
||||
},
|
||||
{
|
||||
"id": "carto_api_key",
|
||||
"env_key": "CARTO_API_KEY",
|
||||
"name": "CARTO Basemaps",
|
||||
"description": "API key for the CARTO raster basemap behind the DEFAULT dark/light map. CARTO requires one; without it tiles still load but carry an \"API KEY REQUIRED\" watermark. Free at carto.com/basemaps/apikey (no CARTO account needed, 5M tiles/month). Unlike the other keys this one is sent to the browser (GET /api/basemap-config) because the browser passes it to CARTO on every tile request.",
|
||||
"category": "Imagery",
|
||||
"url": "https://carto.com/basemaps/apikey",
|
||||
"required": False,
|
||||
},
|
||||
]
|
||||
|
||||
ALLOWED_ENV_KEYS = {
|
||||
@@ -391,6 +400,13 @@ def get_api_keys():
|
||||
return result
|
||||
|
||||
|
||||
def get_basemap_config() -> dict:
|
||||
"""Public config for the browser map: the CARTO key (or empty when unset)."""
|
||||
load_persisted_api_keys_into_environ()
|
||||
key = os.environ.get("CARTO_API_KEY", "").strip()
|
||||
return {"carto": {"configured": bool(key), "key": key}}
|
||||
|
||||
|
||||
def save_api_keys(updates: dict[str, str]) -> dict:
|
||||
"""Persist allowed API keys from a local operator request.
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class Settings(BaseSettings):
|
||||
OPENSKY_CLIENT_ID: str = ""
|
||||
OPENSKY_CLIENT_SECRET: str = ""
|
||||
LTA_ACCOUNT_KEY: str = ""
|
||||
CARTO_API_KEY: str = "" # Basemap tiles; served to the browser via /api/basemap-config
|
||||
|
||||
# Runtime
|
||||
CORS_ORIGINS: str = ""
|
||||
|
||||
@@ -49,6 +49,7 @@ _OPTIONAL = {
|
||||
"AISHUB_USERNAME": "AISHub REST backup when AISStream is silent (optional; free at aishub.net/api)",
|
||||
"GFW_API_TOKEN": "Global Fishing Watch fishing-vessel activity (fishing_activity layer)",
|
||||
"LTA_ACCOUNT_KEY": "Singapore LTA traffic cameras (CCTV layer)",
|
||||
"CARTO_API_KEY": "CARTO basemap tiles (DEFAULT map shows an API KEY REQUIRED watermark without it)",
|
||||
"PUBLIC_API_KEY": "Optional client auth for public endpoints (recommended for exposed deployments)",
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""GET /api/basemap-config serves the CARTO basemap key to the browser.
|
||||
|
||||
The key is public by nature (the browser sends it to CARTO on every tile
|
||||
request), so the endpoint needs no admin auth. It must read the key at
|
||||
request time so Docker operators can set it without a rebuild, and it must
|
||||
honor the persisted operator key file like every other registry key.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from services import api_settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(api_settings, "OPERATOR_KEYS_ENV_PATH", tmp_path / "operator_api_keys.env")
|
||||
monkeypatch.delenv("CARTO_API_KEY", raising=False)
|
||||
import main
|
||||
|
||||
return TestClient(main.app, raise_server_exceptions=False)
|
||||
|
||||
|
||||
def test_unconfigured_when_env_unset(client):
|
||||
r = client.get("/api/basemap-config")
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {"carto": {"configured": False, "key": ""}}
|
||||
|
||||
|
||||
def test_returns_trimmed_key_without_admin_auth(client, monkeypatch):
|
||||
monkeypatch.setenv("CARTO_API_KEY", " carto-test-key ")
|
||||
r = client.get("/api/basemap-config")
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {"carto": {"configured": True, "key": "carto-test-key"}}
|
||||
|
||||
|
||||
def test_reads_persisted_operator_key_file(client, tmp_path):
|
||||
(tmp_path / "operator_api_keys.env").write_text("CARTO_API_KEY=persisted-key\n")
|
||||
r = client.get("/api/basemap-config")
|
||||
assert r.json()["carto"] == {"configured": True, "key": "persisted-key"}
|
||||
|
||||
|
||||
def test_settings_model_exposes_carto_key(monkeypatch):
|
||||
# env_check reads keys off Settings, so the field must exist there or the
|
||||
# startup check would always report CARTO_API_KEY as unset.
|
||||
from services.config import Settings
|
||||
|
||||
monkeypatch.setenv("CARTO_API_KEY", "from-env")
|
||||
assert Settings().CARTO_API_KEY == "from-env"
|
||||
|
||||
|
||||
def test_carto_key_is_in_registry_and_saveable():
|
||||
assert "CARTO_API_KEY" in api_settings.ALLOWED_ENV_KEYS
|
||||
entry = next(a for a in api_settings.API_REGISTRY if a["env_key"] == "CARTO_API_KEY")
|
||||
assert entry["required"] is False
|
||||
Reference in New Issue
Block a user