diff --git a/README.md b/README.md index 335b72c..ace49b6 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,15 @@ Die Bewertungen-Sektion der Startseite zeigt echte Google-Rezensionen, sobald `G 1. **API aktivieren:** In der Cloud Console unter *APIs & Dienste → Bibliothek* nach `Places API (New)` suchen und aktivieren. Es gibt dort zwei fast gleich heißende Einträge — die alte „Places API" ist die falsche, und die „Maps JavaScript API" hilft hier ebenfalls nicht. 2. **Abrechnung aktivieren:** Ohne hinterlegtes Rechnungskonto antwortet auch die neue API mit einem Berechtigungsfehler. Das kostenlose Kontingent deckt diese Nutzung (ein Abruf pro Stunde) bei Weitem ab. 3. **API-Schlüssel erzeugen:** *APIs & Dienste → Anmeldedaten → Anmeldedaten erstellen → API-Schlüssel*. Ein **OAuth-Client ist etwas anderes** und funktioniert hier nicht — er ist für Anmeldungen im Namen eines Nutzers gedacht, nicht für Serverabrufe. -4. **Werte setzen:** Schlüssel als `GOOGLE_PLACES_API_KEY` (Secret), Place-ID als `GOOGLE_PLACE_ID` (ConfigMap). Die Place-ID des eigenen Eintrags findet sich über den [Place ID Finder](https://developers.google.com/maps/documentation/places/web-service/place-id). +4. **Werte setzen:** Schlüssel als `GOOGLE_PLACES_API_KEY` (Secret), Place-ID als `GOOGLE_PLACE_ID` (ConfigMap). + +**Die Place-ID steht nicht in der Cloud Console.** Sie gehört zum Eintrag bei Google Maps, nicht zum Projekt. Mit gesetztem Schlüssel liefert sie dieses Kommando: + +```bash +pnpm places:find "Wabenhain Altenmünster" +``` + +Es gibt Name, Anschrift und `GOOGLE_PLACE_ID=…` je Treffer aus — und ist zugleich die schnellste Probe auf die Einrichtung: kommt eine ID heraus, stimmen API, Abrechnung und Schlüssel. Andernfalls benennt es die Ursache. Ohne Schlüssel geht es auch über den [Place ID Finder](https://developers.google.com/maps/documentation/places/web-service/place-id). Der Schlüssel wird ausschließlich serverseitig verwendet und erscheint nie im Browser — eine Referrer-Beschränkung greift daher **nicht** und würde den Abruf blockieren. Sinnvoll ist stattdessen eine API-Beschränkung auf „Places API (New)" und optional eine IP-Beschränkung auf die ausgehende Adresse des Clusters. diff --git a/package.json b/package.json index 8b2c223..286fc9b 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test:watch": "vitest", "test:e2e": "playwright test", "preflight": "tsx scripts/preflight.ts", + "places:find": "tsx scripts/find-place-id.ts", "orders:release-stale": "tsx scripts/release-stale-orders.ts" }, "dependencies": { diff --git a/scripts/find-place-id.ts b/scripts/find-place-id.ts new file mode 100644 index 0000000..88bdd8e --- /dev/null +++ b/scripts/find-place-id.ts @@ -0,0 +1,102 @@ +/** + * Sucht die Place-ID eines Google-Maps-Eintrags. + * + * Die Place-ID steht NICHT in der Google Cloud Console — dort gibt es nur den + * API-Schluessel. Sie gehoert zum Eintrag bei Google Maps und muss ueber die + * Places API nachgeschlagen werden. Genau das tut dieses Skript. + * + * Es ist zugleich der schnellste Weg, die Einrichtung zu pruefen: kommt hier + * eine ID heraus, sind API, Abrechnung und Schluessel richtig gesetzt und die + * Startseite kann die Rezensionen abrufen. Kommt stattdessen eine + * Fehlermeldung, benennt sie die Ursache — anders als die Meldung der alten + * API, die bei nicht aktivierter Schnittstelle behauptet, der Schluessel sei + * ungueltig. + * + * Aufruf: + * pnpm places:find "Wabenhain Altenmünster" + */ +import "dotenv/config"; + +const SEARCH_URL = "https://places.googleapis.com/v1/places:searchText"; +const TIMEOUT_MS = 10_000; + +interface SearchResponse { + places?: { + id?: string; + displayName?: { text?: string }; + formattedAddress?: string; + }[]; + error?: { message?: string; status?: string }; +} + +function fail(message: string): never { + console.error(`\n✖ ${message}\n`); + process.exit(1); +} + +async function main(): Promise { + const query = process.argv.slice(2).join(" ").trim(); + if (!query) { + fail('Bitte einen Suchbegriff angeben, z.B.: pnpm places:find "Wabenhain Altenmünster"'); + } + + const apiKey = process.env.GOOGLE_PLACES_API_KEY?.trim(); + if (!apiKey) { + fail( + "GOOGLE_PLACES_API_KEY ist nicht gesetzt. Den API-Schluessel aus der Google Cloud Console\n" + + " in die .env eintragen (Anmeldedaten → API-Schluessel, NICHT OAuth-Client)." + ); + } + + const response = await fetch(SEARCH_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Goog-Api-Key": apiKey, + // Nur die drei Felder, die unten ausgegeben werden — die API rechnet + // nach angeforderten Feldern ab. + "X-Goog-FieldMask": "places.id,places.displayName,places.formattedAddress", + }, + body: JSON.stringify({ textQuery: query, languageCode: "de" }), + signal: AbortSignal.timeout(TIMEOUT_MS), + }); + + const data = (await response.json().catch(() => null)) as SearchResponse | null; + + if (!response.ok || data?.error) { + const detail = data?.error?.message ?? `HTTP ${response.status}`; + fail( + `Google hat die Anfrage abgelehnt: ${detail}\n\n` + + " Die drei haeufigsten Ursachen, in dieser Reihenfolge:\n" + + ' 1. "Places API (New)" ist im Projekt nicht aktiviert (die gleichnamige ALTE\n' + + " API reicht nicht — sie laesst sich in neuen Projekten gar nicht mehr\n" + + " aktivieren).\n" + + " 2. Am Projekt ist keine Abrechnung hinterlegt.\n" + + " 3. Der Wert ist ein OAuth-Client statt eines API-Schluessels, oder der\n" + + " Schluessel traegt eine Referrer-Beschraenkung — dieser Abruf laeuft\n" + + " serverseitig und hat keinen Referrer." + ); + } + + const places = data?.places ?? []; + if (places.length === 0) { + fail(`Kein Eintrag zu "${query}" gefunden. Genauer suchen, z.B. mit Ort oder Strasse.`); + } + + console.log(`\nTreffer für "${query}":\n`); + for (const place of places) { + console.log(` ${place.displayName?.text ?? "(ohne Namen)"}`); + console.log(` ${place.formattedAddress ?? "(ohne Adresse)"}`); + console.log(` GOOGLE_PLACE_ID=${place.id ?? "(ohne ID)"}\n`); + } + console.log( + "Den passenden Wert als GOOGLE_PLACE_ID eintragen — in der .env fuer lokal,\n" + + "in k8s/01-configmap.yaml fuer den Cluster.\n" + ); +} + +main().catch((error: unknown) => { + // Netzwerkfehler und Zeitueberschreitungen landen hier; alles Fachliche ist + // oben bereits mit einer deutlicheren Meldung abgefangen. + fail(`Abruf fehlgeschlagen: ${error instanceof Error ? error.message : "Unbekannter Fehler"}`); +});