Next.js 16 static export + PHP 8 API for cPanel/Apache hosting. - Location-aware campus finder (IP estimate, optional precise location, ZIP search) - Three-step Plan Your Visit form with separate email/SMS consent - PHP API: validation, anti-spam, rate limiting, SQLite lead store, Mailchimp and Sinch Engage adapters, idempotent retries via cron - Attribution capture (utm_*, gclid/gbraid/wbraid, fbclid, ttclid, twclid) and consent-aware analytics hooks - Vitest and PHP test suites, cPanel deployment script and docs Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
37 lines
1.5 KiB
PHP
37 lines
1.5 KiB
PHP
<?php
|
|
/**
|
|
* GET /api/locate.php — approximate, non-invasive location for the campus recommendation.
|
|
* Returns city-level coordinates only. Nothing is stored beyond a short hashed cache.
|
|
*/
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/lib/bootstrap.php';
|
|
require_once __DIR__ . '/lib/Validator.php';
|
|
require_once __DIR__ . '/lib/RateLimit.php';
|
|
require_once __DIR__ . '/lib/GeoIp.php';
|
|
|
|
Api::requireMethod('GET');
|
|
|
|
$ip = Api::clientIp();
|
|
$rl = RateLimit::hit(RateLimit::ipBucket('locate', $ip), 30, 600);
|
|
if (!$rl['allowed']) {
|
|
Api::json(['ok' => false, 'reason' => 'rate_limited'], 429, ['Retry-After' => (string)$rl['retryAfter']]);
|
|
}
|
|
|
|
// Local development helper: ?debug_ip=8.8.8.8 (only when DEBUG_GEO=true)
|
|
if (Config::bool('DEBUG_GEO', false) && !empty($_GET['debug_ip']) && filter_var($_GET['debug_ip'], FILTER_VALIDATE_IP)) {
|
|
$ip = (string)$_GET['debug_ip'];
|
|
}
|
|
if (Config::bool('DEBUG_GEO', false) && isset($_GET['debug_lat'], $_GET['debug_lng'])) {
|
|
Api::json(['ok' => true, 'approximate' => ['lat' => (float)$_GET['debug_lat'], 'lng' => (float)$_GET['debug_lng'], 'city' => 'Debug', 'confidence' => 'low']]);
|
|
}
|
|
|
|
$loc = GeoIp::locate($ip);
|
|
if (!$loc) {
|
|
Api::json(['ok' => false, 'reason' => 'unavailable'], 200, ['Cache-Control' => 'private, max-age=300']);
|
|
}
|
|
Api::json([
|
|
'ok' => true,
|
|
'approximate' => ['lat' => $loc['lat'], 'lng' => $loc['lng'], 'city' => $loc['city'], 'region' => $loc['region'], 'confidence' => 'low'],
|
|
], 200, ['Cache-Control' => 'private, max-age=3600']);
|