Add public availability API and settings panel

Public saatavuus endpoint with API key + CORS protection for
cuitunet.fi website integration. Admin settings tab for API key
management and testing. Includes standalone widget page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 01:50:52 +02:00
parent 8ba925d3dc
commit 14707b9616
5 changed files with 341 additions and 0 deletions

137
api.php
View File

@@ -17,6 +17,7 @@ define('ARCHIVE_FILE', DATA_DIR . '/archive.json');
define('LEADS_FILE', DATA_DIR . '/leads.json');
define('TOKENS_FILE', DATA_DIR . '/reset_tokens.json');
define('RATE_FILE', DATA_DIR . '/login_attempts.json');
define('CONFIG_FILE', DATA_DIR . '/config.json');
define('SITE_URL', 'https://intra.cuitunet.fi');
// Sähköpostiasetukset
@@ -88,6 +89,23 @@ function getClientIp(): string {
return $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
// ==================== CONFIG ====================
function loadConfig(): array {
if (!file_exists(CONFIG_FILE)) return [];
return json_decode(file_get_contents(CONFIG_FILE), true) ?: [];
}
function saveConfig(array $config): void {
file_put_contents(CONFIG_FILE, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
function normalizeAddress(string $addr): string {
$addr = mb_strtolower(trim($addr));
$addr = preg_replace('/\s+/', ' ', $addr);
return $addr;
}
// ==================== EMAIL ====================
function sendMail(string $to, string $subject, string $htmlBody): bool {
@@ -247,6 +265,125 @@ function parseLiittymat(array $input): array {
switch ($action) {
// ---------- SAATAVUUS (julkinen API) ----------
case 'saatavuus':
// CORS - salli cuitunet.fi
$config = loadConfig();
$allowedOrigins = $config['cors_origins'] ?? ['https://cuitunet.fi', 'https://www.cuitunet.fi'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Api-Key');
}
if ($method === 'OPTIONS') { http_response_code(204); break; }
// API-avain tarkistus
$apiKey = $config['api_key'] ?? '';
$providedKey = $_GET['key'] ?? ($_SERVER['HTTP_X_API_KEY'] ?? '');
if (empty($apiKey) || $providedKey !== $apiKey) {
http_response_code(403);
echo json_encode(['error' => 'Virheellinen API-avain']);
break;
}
$query = normalizeAddress($_GET['osoite'] ?? '');
$postinumero = trim($_GET['postinumero'] ?? '');
if (empty($query) && empty($postinumero)) {
http_response_code(400);
echo json_encode(['error' => 'Anna osoite tai postinumero']);
break;
}
$customers = loadCustomers();
$matches = [];
foreach ($customers as $c) {
foreach ($c['liittymat'] ?? [] as $l) {
$addr = normalizeAddress($l['asennusosoite'] ?? '');
$zip = trim($l['postinumero'] ?? '');
$city = mb_strtolower(trim($l['kaupunki'] ?? ''));
$hit = false;
// Postinumero-haku
if (!empty($postinumero) && $zip === $postinumero) {
$hit = true;
}
// Osoitehaku (sisältää haun)
if (!empty($query) && !empty($addr)) {
if (str_contains($addr, $query) || str_contains($query, $addr)) {
$hit = true;
}
// Kadunnimi-match (ilman numeroa)
$queryStreet = preg_replace('/\d+.*$/', '', $query);
$addrStreet = preg_replace('/\d+.*$/', '', $addr);
if (!empty(trim($queryStreet)) && !empty(trim($addrStreet)) && str_contains(trim($addrStreet), trim($queryStreet))) {
$hit = true;
}
}
if ($hit) {
// Palauta VAIN osoitetieto ja nopeus - ei asiakastietoja
$matches[] = [
'osoite' => $l['asennusosoite'] ?? '',
'postinumero' => $zip,
'kaupunki' => $l['kaupunki'] ?? '',
'nopeus' => $l['liittymanopeus'] ?? '',
];
}
}
}
// Poista duplikaatit (sama osoite eri asiakkailla)
$unique = [];
$seen = [];
foreach ($matches as $m) {
$key = normalizeAddress($m['osoite'] . $m['postinumero']);
if (!isset($seen[$key])) {
$unique[] = $m;
$seen[$key] = true;
}
}
echo json_encode([
'saatavilla' => count($unique) > 0,
'kohteet' => $unique,
'maara' => count($unique),
]);
break;
// ---------- CONFIG (admin) ----------
case 'config':
requireAdmin();
echo json_encode(loadConfig());
break;
case 'config_update':
requireAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$config = loadConfig();
if (isset($input['api_key'])) $config['api_key'] = trim($input['api_key']);
if (isset($input['cors_origins'])) {
$origins = array_filter(array_map('trim', explode("\n", $input['cors_origins'])));
$config['cors_origins'] = array_values($origins);
}
saveConfig($config);
addLog('config_update', '', '', 'Päivitti asetukset');
echo json_encode($config);
break;
case 'generate_api_key':
requireAdmin();
if ($method !== 'POST') break;
$config = loadConfig();
$config['api_key'] = bin2hex(random_bytes(16));
saveConfig($config);
addLog('config_update', '', '', 'Generoi uuden API-avaimen');
echo json_encode($config);
break;
// ---------- CAPTCHA ----------
case 'captcha':
$a = rand(1, 20);