diff --git a/.gitignore b/.gitignore
index 31f0b7e..6483aa2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ data/users.json
data/changelog.json
data/archive.json
data/leads.json
+data/config.json
data/reset_tokens.json
data/login_attempts.json
data/backups/
diff --git a/api.php b/api.php
index 11f86fb..e6cae0b 100644
--- a/api.php
+++ b/api.php
@@ -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);
diff --git a/index.html b/index.html
index f3bca14..ee6a01c 100644
--- a/index.html
+++ b/index.html
@@ -76,6 +76,7 @@
+
@@ -258,6 +259,51 @@
+
+
+
+
+
Saatavuus-API
+
Julkinen API jolla cuitunet.fi voi tarkistaa kuituverkon saatavuuden osoitteessa. Palauttaa vain osoite + nopeus - ei asiakastietoja.
+
+
API-ohjeet
+
+
Endpoint:
GET https://intra.cuitunet.fi/api.php?action=saatavuus
+
Parametrit:
+ • key = API-avain (pakollinen)
+ • osoite = Haettava osoite (esim. "Kauppakatu 5")
+ • postinumero = Postinumero (esim. "20100")
+ Anna vähintään toinen: osoite tai postinumero.
+
Esimerkki:
+ api.php?action=saatavuus&key=AVAIN&osoite=Kauppakatu+5
+
Vastaus:
+ {"saatavilla":true,"kohteet":[{"osoite":"...","postinumero":"...","kaupunki":"...","nopeus":"..."}],"maara":1}
+
+
Testaa API
+
+
+
+
+
+
+
+
+
diff --git a/saatavuus-widget.html b/saatavuus-widget.html
new file mode 100644
index 0000000..b476694
--- /dev/null
+++ b/saatavuus-widget.html
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+ Kuituverkon saatavuus - CuituNet
+
+
+
+
+
Tarkista kuituverkon saatavuus
+
Syötä osoitteesi tai postinumerosi ja tarkista onko kuituliittymä saatavilla.
+
+
+
+
+
+
+
+
+
+
diff --git a/script.js b/script.js
index 90b4826..bf33b02 100644
--- a/script.js
+++ b/script.js
@@ -169,6 +169,7 @@ async function showDashboard() {
document.getElementById('user-info').textContent = currentUser.nimi || currentUser.username;
// Näytä Käyttäjät-tab vain adminille
document.getElementById('tab-users').style.display = currentUser.role === 'admin' ? '' : 'none';
+ document.getElementById('tab-settings').style.display = currentUser.role === 'admin' ? '' : 'none';
await loadCustomers();
}
@@ -186,6 +187,7 @@ document.querySelectorAll('.tab').forEach(tab => {
if (target === 'archive') loadArchive();
if (target === 'changelog') loadChangelog();
if (target === 'users') loadUsers();
+ if (target === 'settings') loadSettings();
});
});
@@ -818,6 +820,7 @@ const actionLabels = {
lead_update: 'Muokkasi liidiä',
lead_delete: 'Poisti liidin',
lead_to_customer: 'Muutti liidin asiakkaaksi',
+ config_update: 'Päivitti asetukset',
};
async function loadChangelog() {
@@ -917,6 +920,52 @@ document.getElementById('user-form').addEventListener('submit', async (e) => {
} catch (e) { alert(e.message); }
});
+// ==================== SETTINGS ====================
+
+async function loadSettings() {
+ try {
+ const config = await apiCall('config');
+ document.getElementById('settings-api-key').value = config.api_key || '';
+ document.getElementById('settings-cors').value = (config.cors_origins || ['https://cuitunet.fi', 'https://www.cuitunet.fi']).join('\n');
+ const key = config.api_key || 'AVAIN';
+ document.getElementById('api-example-url').textContent = `api.php?action=saatavuus&key=${key}&osoite=Kauppakatu+5`;
+ } catch (e) { console.error(e); }
+}
+
+document.getElementById('btn-generate-key').addEventListener('click', async () => {
+ try {
+ const config = await apiCall('generate_api_key', 'POST');
+ document.getElementById('settings-api-key').value = config.api_key || '';
+ document.getElementById('api-example-url').textContent = `api.php?action=saatavuus&key=${config.api_key}&osoite=Kauppakatu+5`;
+ } catch (e) { alert(e.message); }
+});
+
+document.getElementById('btn-save-settings').addEventListener('click', async () => {
+ try {
+ const config = await apiCall('config_update', 'POST', {
+ api_key: document.getElementById('settings-api-key').value,
+ cors_origins: document.getElementById('settings-cors').value,
+ });
+ alert('Asetukset tallennettu!');
+ } catch (e) { alert(e.message); }
+});
+
+document.getElementById('btn-test-api').addEventListener('click', async () => {
+ const address = document.getElementById('test-api-address').value.trim();
+ const apiKey = document.getElementById('settings-api-key').value;
+ if (!address) { alert('Anna osoite tai postinumero'); return; }
+ const result = document.getElementById('test-api-result');
+ result.style.display = 'block';
+ result.textContent = 'Haetaan...';
+ try {
+ const isZip = /^\d{5}$/.test(address);
+ const param = isZip ? `postinumero=${encodeURIComponent(address)}` : `osoite=${encodeURIComponent(address)}`;
+ const res = await fetch(`${API}?action=saatavuus&key=${encodeURIComponent(apiKey)}&${param}`);
+ const data = await res.json();
+ result.textContent = JSON.stringify(data, null, 2);
+ } catch (e) { result.textContent = 'Virhe: ' + e.message; }
+});
+
// ==================== MODALS ====================
customerModal.addEventListener('click', (e) => { if (e.target === customerModal) customerModal.style.display = 'none'; });