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 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-27 08:51:51 -05:00
co-authored by Claude Sonnet 4.6
parent abc19505ae
commit d3ca99fa1b
+83
View File
@@ -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<string | null> {
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',
},
});
}