import type { NextRequest } from "next/server"; import { OWM_TILE_LAYERS } from "@/lib/radar/layers"; import { forbidCrossSite } from "@/lib/weather/respond"; const CARTO_LAYERS: Record = { "carto-dark-nolabels": "dark_nolabels", "carto-dark-labels": "dark_only_labels", }; const CARTO_SUBDOMAINS = ["a", "b", "c", "d"] as const; const MAX_ZOOM = 18; /** * GET /api/tiles/[layer]/[z]/[x]/[y] → one map tile (PNG). * * Proxies both OpenWeather weather overlays (OPENWEATHER_API_KEY) and CARTO * dark basemap/label tiles (CARTO_API_KEY) server-side so keys stay in `.env` * or `.env.local` and cannot be hotlinked from other domains. */ export async function GET( request: NextRequest, context: { params: Promise<{ layer: string; z: string; x: string; y: string }> }, ) { const blocked = forbidCrossSite(request); if (blocked) return blocked; const { layer, z, x, y } = await context.params; const [zi, xi, yi] = [z, x, y].map(Number); const maxIndex = 2 ** zi; if ( ![zi, xi, yi].every((n) => Number.isInteger(n) && n >= 0) || zi > MAX_ZOOM || xi >= maxIndex || yi >= maxIndex ) { return new Response("Bad tile coordinates", { status: 400 }); } // 1. CARTO dark basemap & place-label tiles const cartoStyle = CARTO_LAYERS[layer]; if (cartoStyle) { const key = (process.env.CARTO_API_KEY ?? process.env.NEXT_PUBLIC_CARTO_API_KEY ?? "").trim(); const scale = request.nextUrl.searchParams.get("r") === "@2x" ? "@2x" : ""; const sub = CARTO_SUBDOMAINS[(xi + yi) % CARTO_SUBDOMAINS.length]; const query = key ? `?key=${encodeURIComponent(key)}` : ""; const url = `https://${sub}.basemaps.cartocdn.com/${cartoStyle}/${z}/${x}/${y}${scale}.png${query}`; const headers: HeadersInit = {}; const referer = request.headers.get("referer"); const origin = request.headers.get("origin"); if (referer) headers.Referer = referer; if (origin) headers.Origin = origin; const upstream = await fetch( url, key ? { headers, next: { revalidate: 86_400 } } : { headers, cache: "no-store" }, ).catch(() => null); if (!upstream?.ok) return new Response("Tile unavailable", { status: 502 }); // CARTO tags unauthenticated placeholder tiles with an `"wm-..."` ETag; never cache those. const isWatermarked = !key || Boolean(upstream.headers.get("etag")?.includes("wm-")); return new Response(upstream.body, { headers: { "Content-Type": "image/png", "Cache-Control": isWatermarked ? "no-store" : "public, max-age=86400", }, }); } // 2. OpenWeather weather-model overlay tiles const owmLayer = OWM_TILE_LAYERS[layer]; const owmKey = process.env.OPENWEATHER_API_KEY?.trim(); if (!owmLayer) return new Response("Unknown layer", { status: 404 }); if (!owmKey) return new Response("OPENWEATHER_API_KEY is not set", { status: 503 }); const upstream = await fetch(`https://tile.openweathermap.org/map/${owmLayer}/${z}/${x}/${y}.png?appid=${owmKey}`, { next: { revalidate: 600 }, }).catch(() => null); if (!upstream?.ok) return new Response("Tile unavailable", { status: 502 }); return new Response(upstream.body, { headers: { "Content-Type": "image/png", "Cache-Control": "public, max-age=600", }, }); }