diff --git a/public/api-proxy.php b/public/api-proxy.php index 06524ca..54027cf 100644 --- a/public/api-proxy.php +++ b/public/api-proxy.php @@ -2,13 +2,12 @@ declare(strict_types=1); require_once __DIR__ . '/../src/Calco2latoApiClient.php'; -// --- Basic CORS (adjust origin to your site/domain) --- +// --- CORS (restrict to your site) --- $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; -$allowedOrigin = preg_match('#^https://(www\.)?your-frontend\.example$#', $origin) ? $origin : ''; -if ($allowedOrigin) { - header('Access-Control-Allow-Origin: ' . $allowedOrigin); +if (preg_match('#^https://(www\.)?your-frontend\.example$#', $origin)) { + header('Access-Control-Allow-Origin: ' . $origin); header('Vary: Origin'); - header('Access-Control-Allow-Credentials: true'); + header('Access-Control-Allow-Credentials', 'true'); } header('Access-Control-Allow-Headers: Content-Type'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); @@ -55,7 +54,9 @@ if (!$key) { } // --- Instantiate API client --- -$client = new Calco2latoApiClient($base, $key); +// Version from query (?ver=v1|test|latest), default latest +$ver = $_GET['ver'] ?? $_POST['ver'] ?? Calco2latoApiVersion::LATEST; +$client = new Calco2latoApiClient($base, $key, (string)$ver); // --- Whitelist router --- $input = json_decode(file_get_contents('php://input') ?: '[]', true) ?: []; @@ -66,21 +67,32 @@ header('Content-Type: application/json; charset=utf-8'); try { switch ($endpoint) { - case 'airports.search': - // GET /?endpoint=airports.search&q=FRA&per_page=10 - $q = $_GET['q'] ?? ''; - $per_page = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 20; - $page = isset($_GET['page']) ? (int)$_GET['page'] : 1; - $data = $client->searchAirports($q, $per_page, $page); - echo json_encode($data); + case 'health': + echo json_encode(['ok'=>true,'ver'=>$ver]); break; + case 'airports.search': + if ($method === 'GET') { + $q = [ + 'page' => isset($_GET['page']) ? (int)$_GET['page'] : null, + 'per_page' => isset($_GET['per_page']) ? (int)$_GET['per_page'] : null, + 'sort_by' => $_GET['sort_by'] ?? null, + 'order' => $_GET['order'] ?? null, + 'iata' => $_GET['iata'] ?? null, + ]; + echo json_encode($client->airports_get($q)); + break; + } elseif ($method === 'POST') { + $params = $input['params'] ?? []; + echo json_encode($client->airports_post($params)); + break; + } + case 'flights.estimate': // POST with JSON body: { endpoint: "flights.estimate", params: {...} } if ($method !== 'POST') throw new RuntimeException('Use POST'); $params = $input['params'] ?? []; - $data = $client->flightEstimate($params); - echo json_encode($data); + echo json_encode($client->flight_post($params)); break; default: diff --git a/public/example.html b/public/example.html index bdc7382..fab6cbd 100644 --- a/public/example.html +++ b/public/example.html @@ -19,7 +19,7 @@

Search for airports

- +
@@ -55,8 +55,8 @@ e.preventDefault(); list.innerHTML = 'Loading…'; try { - const q = new FormData(searchform).get('q'); - const airports = await api.searchAirports(q, 10, 1); + const iata = new FormData(searchform).get('iata'); + const airports = await api.searchAirports(iata, 10, 1); list.innerHTML = airports.map(a => `
  • ${a.display}
  • `).join(''); } catch (err) { list.innerHTML = `
  • ${err.message}
  • `; @@ -72,7 +72,7 @@ try { const f = new FormData(flightform); const flight = await api.estimateFlight({"flights": [{"departure": f.get('departure'), "arrival": f.get('arrival'), "passengerCount": 1, "travelClass": f.get('cabinclass'), "departureDate": f.get('departuredate')}]}); - flightresult.innerHTML = JSON.stringify(flight, null, 2); + flightresult.innerHTML = flight.summary(); } catch (err) { flightresult.innerHTML = `${err.message}`; } diff --git a/public/js/calco2lato.js b/public/js/calco2lato.js index 1a7d868..a2a60a2 100644 --- a/public/js/calco2lato.js +++ b/public/js/calco2lato.js @@ -17,7 +17,7 @@ export class Flight { } summary() { const s = []; - if (this.departureAirport?.iata) s.push(this.departureAirport.iata); + if (this.departureAirport?.iata) s.push(this.departureAirport.iata); if (this.arrivalAirport?.iata) s.push(this.arrivalAirport.iata); const route = s.length ? s.join(' → ') : 'Flight'; const co2 = this.emissions?.co2_total ?? this.co2 ?? null; @@ -77,9 +77,9 @@ export class Calco2latoClient { async searchAirports(q, limit = 20, offset = 1) { const data = await this._fetchJSON({ method: 'GET', - query: { endpoint: 'airports.search', q, limit, offset } + query: { endpoint: 'airports.search', iata: q, per_page: limit, page: offset } }); - const items = Array.isArray(data?.results) ? data.results : (Array.isArray(data) ? data : []); + const items = Array.isArray(data?.results) ? data.results : (Array.isArray(data) ? data : []); return items.map(a => new Airport(a)); } @@ -94,6 +94,6 @@ export class Calco2latoClient { method: 'POST', body: { endpoint: 'flights.estimate', params } }); - return new Flight(data); + return new Flight(data.flights[0]); } } diff --git a/src/Calco2latoApiClient.php b/src/Calco2latoApiClient.php index c60bf23..cf87382 100644 --- a/src/Calco2latoApiClient.php +++ b/src/Calco2latoApiClient.php @@ -1,27 +1,56 @@ baseUrl = rtrim($baseUrl, '/'); $this->apiKey = $apiKey; + $this->version = Calco2latoApiVersion::ensure($version); $this->timeout = $timeoutSeconds; } - /** - * Low-level request wrapper (GET/POST/DELETE/PUT/PATCH). - * $query is appended to URL; $body (if provided) is JSON-encoded. - */ - private function request(string $method, string $path, array $query = [], ?array $body = null): array + public function withVersion(string $version): self { - $url = $this->baseUrl . '/' . ltrim($path, '/'); - if (!empty($query)) { + $clone = clone $this; + $clone->version = Calco2latoApiVersion::ensure($version); + return $clone; + } + + private function url(string $path): string + { + $path = '/' . ltrim($path, '/'); + return "{$this->baseUrl}/{$this->version}{$path}"; + } + + /** @return array */ + private function request(string $method, string $path, array $query = [], ?array $json = null): array + { + $url = $this->url($path); + if ($query) { $url .= (str_contains($url, '?') ? '&' : '?') . http_build_query($query); } @@ -30,62 +59,73 @@ final class Calco2latoApiClient 'Accept: application/json', 'Authorization: Bearer ' . $this->apiKey, ]; - curl_setopt_array($ch, [ + + $opts = [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => strtoupper($method), CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => $this->timeout, - ]); + ]; - if ($body !== null) { - $json = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($json !== null) { + $payload = json_encode($json, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $headers[] = 'Content-Type: application/json'; - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_POSTFIELDS, $json); + $opts[CURLOPT_HTTPHEADER] = $headers; + $opts[CURLOPT_POSTFIELDS] = $payload; } - $responseBody = curl_exec($ch); - $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $err = curl_error($ch); + curl_setopt_array($ch, $opts); + $raw = curl_exec($ch); + $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $err = curl_error($ch); curl_close($ch); - if ($responseBody === false) { + if ($raw === false) { throw new RuntimeException('cURL error: ' . $err); } - $decoded = json_decode($responseBody, true); + $decoded = json_decode($raw, true); if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { - throw new RuntimeException('Invalid JSON from API (HTTP ' . $status . '): ' . $responseBody); + throw new RuntimeException("Invalid JSON from API (HTTP {$status}): {$raw}"); } - if ($status < 200 || $status >= 300) { - $msg = $decoded['error'] ?? $decoded['message'] ?? 'Upstream API error'; - throw new RuntimeException('API error (HTTP ' . $status . '): ' . $msg); + $msg = is_array($decoded) ? ($decoded['error'] ?? $decoded['message'] ?? 'Upstream API error') : 'Upstream API error'; + throw new RuntimeException("API error (HTTP {$status}): {$msg}"); } + /** @var array $decoded */ return $decoded; } - // ---------- Airports ---------- + // ===================== Airports ===================== - /** Search airports by free-text (IATA/ICAO/name/city/country) */ - public function searchAirports(string $q, int $per_page = 20, int $page = 0): array + /** GET /{v}/airports — query: page, per_page, sort_by, order, iata */ + public function airports_get(array $query = []): array { - return $this->request('GET', '/latest/transport/airports', [ - 'iata' => $q, - 'per_page' => $per_page, - 'page' => $page, - ]); + // Only pass the fields your spec declares (extra keys are harmless but we keep it tidy) + $allowed = ['page','per_page','sort_by','order','iata']; + $q = []; + foreach ($allowed as $k) { + if (array_key_exists($k, $query) && $query[$k] !== null) { + $q[$k] = $query[$k]; + } + } + return $this->request('GET', '/transport/airports', $q, null); } - // ---------- Flights ---------- - - /** - * Get flight estimate / emissions (example – adjust to your API) - * $params might include origin, destination, date, pax, class, etc. - */ - public function flightEstimate(array $params): array + /** POST /{v}/airports — body: AirportsRequest */ + public function airports_post(array $airportsRequest = []): array { - return $this->request('POST', '/latest/transport/flight', [], $params); + // Accepts the exact schema: page, per_page, sort_by, order, iata + return $this->request('POST', '/transport/airports', [], $airportsRequest); } -} + + // ===================== Flight ===================== + + /** POST /{v}/flight — body: FlightRequest */ + public function flight_post(array $flightRequest): array + { + // The spec requires "flights" in the body; api_key could be included, but we already send Bearer + return $this->request('POST', '/transport/flight', [], $flightRequest); + } +} \ No newline at end of file