From d3ca99fa1b14b61d1a4a4bb41a91fae49c56397e Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Sat, 27 Jun 2026 08:51:51 -0500 Subject: [PATCH] feat: agregar endpoint /autocomplete en admin Next.js Proxy a Google Places Autocomplete API: - Lee la API key de /settings/maps-key del backend - Acepta params: input, location - Devuelve { results: [{ formatted_address, place_id }] } - Headers CORS para permitir acceso desde app.prosapp.co Co-Authored-By: Claude Sonnet 4.6 --- admin/src/app/autocomplete/route.ts | 83 +++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 admin/src/app/autocomplete/route.ts diff --git a/admin/src/app/autocomplete/route.ts b/admin/src/app/autocomplete/route.ts new file mode 100644 index 0000000..30eeb28 --- /dev/null +++ b/admin/src/app/autocomplete/route.ts @@ -0,0 +1,83 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api/v1'; + +// Cache the Maps API key in memory (refreshes every 5 min) +let cachedKey: string | null = null; +let cacheTime = 0; + +async function getMapsKey(): Promise { + const now = Date.now(); + if (cachedKey && now - cacheTime < 5 * 60 * 1000) return cachedKey; + try { + const res = await fetch(`${API_BASE}/settings/maps-key`, { cache: 'no-store' }); + if (!res.ok) return null; + const data = await res.json(); + cachedKey = data.api_key || null; + cacheTime = now; + return cachedKey; + } catch { + return null; + } +} + +export async function GET(req: NextRequest) { + const { searchParams } = req.nextUrl; + const input = searchParams.get('input')?.trim(); + const location = searchParams.get('location') ?? ''; + + if (!input || input.length < 2) { + return NextResponse.json({ results: [] }); + } + + const apiKey = await getMapsKey(); + if (!apiKey) { + return NextResponse.json( + { error: 'Google Maps API key not configured' }, + { status: 503 }, + ); + } + + // Use Places Autocomplete for typed suggestions + const placesUrl = new URL('https://maps.googleapis.com/maps/api/place/autocomplete/json'); + placesUrl.searchParams.set('input', input); + placesUrl.searchParams.set('key', apiKey); + placesUrl.searchParams.set('language', 'es'); + placesUrl.searchParams.set('types', 'address'); + if (location) placesUrl.searchParams.set('location', location); + if (location) placesUrl.searchParams.set('radius', '50000'); + + try { + const res = await fetch(placesUrl.toString(), { cache: 'no-store' }); + const data = await res.json(); + + if (data.status !== 'OK' && data.status !== 'ZERO_RESULTS') { + return NextResponse.json({ results: [] }); + } + + // Normalize predictions → results with formatted_address for Flutter compatibility + const results = (data.predictions ?? []).map((p: any) => ({ + formatted_address: p.description, + place_id: p.place_id, + })); + + return NextResponse.json({ results }, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET', + }, + }); + } catch { + return NextResponse.json({ results: [] }); + } +} + +export async function OPTIONS() { + return new NextResponse(null, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }); +}