fix(maps): let users and admins enter a CARTO API key (#2060)

CARTO stamps an "API KEY REQUIRED" watermark onto every basemap tile
fetched without a key since 26.08.2026, and both CartoDB presets plus the
built-in fallback are CARTO. The key is free and needs no account, so add
it as a setting rather than dropping the provider.

carto_api_key sits next to mapbox_access_token: per user in Settings/Map,
instance-wide under Admin/User Defaults, encrypted at rest, injected from
CARTO_API_KEY on managed instances. Unlike Mapbox it never switches the
map provider, since a raster key says nothing about the renderer.

The key is appended as ?key= at render time instead of being stored in the
template, so a saved URL stays portable and survives a rotation. That is
also what reaches the four maps that pinned CARTO and ignored the user's
tile setting: atlas, collections, the public trip share and the public
journey. The two share pages read it from their token-gated payload, the
same way baseCurrency already travels.

Also lifts the release notice above page chrome. Its overlay sat at
z-index 50, under the Vacay mode toolbar at 61, so the dialog was neither
on top nor modal. Adds a six-step layer scale to index.css and moves the
three notice shells onto it.

Closes #2054
Closes #2052
This commit is contained in:
Maurice
2026-08-27 12:06:55 +02:00
committed by GitHub
parent 521e52c853
commit 1f66ef1d32
99 changed files with 1258 additions and 67 deletions
+16 -5
View File
@@ -38,15 +38,23 @@ export const SENSITIVE_KEYS = new Set([
*/
const SENSITIVE_SUFFIXES = ['_token', '_key', '_secret', '_password', '_pass', '_verifier'];
function isSensitiveName(name: string): boolean {
const lower = name.toLowerCase();
return SENSITIVE_KEYS.has(lower) || SENSITIVE_SUFFIXES.some((suffix) => lower.endsWith(suffix));
}
/** Deep-redacts every key in `SENSITIVE_KEYS` (case-insensitive) from a request-log value. */
export function redact(value: unknown): unknown {
if (!value || typeof value !== 'object') return value;
if (Array.isArray(value)) return (value as unknown[]).map(redact);
const entries = value as Record<string, unknown>;
// PUT /api/settings names the setting instead of using it as the field:
// {key: 'carto_api_key', value: '<secret>'}. Neither field name is sensitive
// on its own, so without this the secret goes into the debug log in cleartext.
const namedSecret = typeof entries.key === 'string' && isSensitiveName(entries.key);
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
const lower = k.toLowerCase();
const sensitive = SENSITIVE_KEYS.has(lower) || SENSITIVE_SUFFIXES.some((suffix) => lower.endsWith(suffix));
out[k] = sensitive ? '[REDACTED]' : redact(v);
for (const [k, v] of Object.entries(entries)) {
out[k] = isSensitiveName(k) || (namedSecret && k === 'value') ? '[REDACTED]' : redact(v);
}
return out;
}
@@ -181,7 +189,10 @@ export function applyGlobalMiddleware(
"https://nominatim.openstreetmap.org", "https://overpass-api.de",
"https://places.googleapis.com", "https://api.openweathermap.org",
"https://en.wikipedia.org", "https://commons.wikimedia.org",
"https://*.basemaps.cartocdn.com",
// Both forms here too: CARTO documents the apex host on its key page,
// so that is the template users paste in, and the {s} sharded form is
// what TREK ships (#2054).
"https://basemaps.cartocdn.com", "https://*.basemaps.cartocdn.com",
// Both forms: a CSP wildcard host never matches the apex, and OSM
// serves everything from the bare tile.openstreetmap.org since it
// retired the a/b/c/d shards (#1733). The sharded hosts stay listed