'Kirjaudu sisään']); exit; } } function requireAdmin() { requireAuth(); if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); echo json_encode(['error' => 'Vain ylläpitäjä voi tehdä tämän']); exit; } } function currentUser(): string { return $_SESSION['username'] ?? 'tuntematon'; } function generateId(): string { return bin2hex(random_bytes(8)); } // ==================== USERS ==================== function initUsers(): void { $users = json_decode(file_get_contents(USERS_FILE), true) ?: []; if (empty($users)) { $users[] = [ 'id' => generateId(), 'username' => 'admin', 'password_hash' => password_hash('cuitunet2024', PASSWORD_DEFAULT), 'nimi' => 'Ylläpitäjä', 'role' => 'admin', 'luotu' => date('Y-m-d H:i:s'), ]; file_put_contents(USERS_FILE, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } } function loadUsers(): array { return json_decode(file_get_contents(USERS_FILE), true) ?: []; } function saveUsers(array $users): void { file_put_contents(USERS_FILE, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } // ==================== CHANGELOG ==================== function addLog(string $action, string $customerId = '', string $customerName = '', string $details = ''): void { $log = json_decode(file_get_contents(CHANGELOG_FILE), true) ?: []; array_unshift($log, [ 'id' => generateId(), 'timestamp' => date('Y-m-d H:i:s'), 'user' => currentUser(), 'action' => $action, 'customer_id' => $customerId, 'customer_name' => $customerName, 'details' => $details, ]); // Säilytä max 500 lokimerkintää if (count($log) > 500) $log = array_slice($log, 0, 500); file_put_contents(CHANGELOG_FILE, json_encode($log, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } // ==================== CUSTOMERS ==================== function loadCustomers(): array { $data = file_get_contents(DATA_FILE); $customers = json_decode($data, true) ?: []; $migrated = false; foreach ($customers as &$c) { if (!isset($c['liittymat'])) { $c['liittymat'] = [[ 'asennusosoite' => $c['asennusosoite'] ?? '', 'postinumero' => $c['postinumero'] ?? '', 'kaupunki' => $c['kaupunki'] ?? '', 'liittymanopeus' => $c['liittymanopeus'] ?? '', 'hinta' => floatval($c['hinta'] ?? 0), 'sopimuskausi' => '', 'alkupvm' => '', ]]; unset($c['asennusosoite'], $c['postinumero'], $c['kaupunki'], $c['liittymanopeus'], $c['hinta']); $migrated = true; } } unset($c); if ($migrated) { file_put_contents(DATA_FILE, json_encode($customers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } return $customers; } function saveCustomers(array $customers): void { if (file_exists(DATA_FILE) && filesize(DATA_FILE) > 2) { $backupDir = DATA_DIR . '/backups'; if (!file_exists($backupDir)) mkdir($backupDir, 0755, true); copy(DATA_FILE, $backupDir . '/customers_' . date('Y-m-d_His') . '.json'); $backups = glob($backupDir . '/customers_*.json'); if (count($backups) > 30) { sort($backups); array_map('unlink', array_slice($backups, 0, count($backups) - 30)); } } file_put_contents(DATA_FILE, json_encode($customers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } // ==================== ARCHIVE ==================== function loadArchive(): array { return json_decode(file_get_contents(ARCHIVE_FILE), true) ?: []; } function saveArchive(array $archive): void { file_put_contents(ARCHIVE_FILE, json_encode($archive, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } function parseLiittymat(array $input): array { $liittymat = []; foreach (($input['liittymat'] ?? []) as $l) { $liittymat[] = [ 'asennusosoite' => trim($l['asennusosoite'] ?? ''), 'postinumero' => trim($l['postinumero'] ?? ''), 'kaupunki' => trim($l['kaupunki'] ?? ''), 'liittymanopeus' => trim($l['liittymanopeus'] ?? ''), 'hinta' => floatval($l['hinta'] ?? 0), 'sopimuskausi' => trim($l['sopimuskausi'] ?? ''), 'alkupvm' => trim($l['alkupvm'] ?? ''), ]; } if (empty($liittymat)) { $liittymat[] = ['asennusosoite' => '', 'postinumero' => '', 'kaupunki' => '', 'liittymanopeus' => '', 'hinta' => 0, 'sopimuskausi' => '', 'alkupvm' => '']; } return $liittymat; } // ==================== ROUTES ==================== switch ($action) { // ---------- AUTH ---------- case 'login': if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $username = trim($input['username'] ?? ''); $password = $input['password'] ?? ''; $users = loadUsers(); $found = false; foreach ($users as $u) { if ($u['username'] === $username && password_verify($password, $u['password_hash'])) { $_SESSION['user_id'] = $u['id']; $_SESSION['username'] = $u['username']; $_SESSION['nimi'] = $u['nimi']; $_SESSION['role'] = $u['role']; echo json_encode(['success' => true, 'username' => $u['username'], 'nimi' => $u['nimi'], 'role' => $u['role']]); $found = true; break; } } if (!$found) { http_response_code(401); echo json_encode(['error' => 'Väärä käyttäjätunnus tai salasana']); } break; case 'logout': session_destroy(); echo json_encode(['success' => true]); break; case 'check_auth': if (isset($_SESSION['user_id'])) { echo json_encode([ 'authenticated' => true, 'username' => $_SESSION['username'], 'nimi' => $_SESSION['nimi'], 'role' => $_SESSION['role'], ]); } else { echo json_encode(['authenticated' => false]); } break; // ---------- USERS ---------- case 'users': requireAdmin(); $users = loadUsers(); // Älä palauta salasanoja $safe = array_map(function($u) { unset($u['password_hash']); return $u; }, $users); echo json_encode(array_values($safe)); break; case 'user_create': requireAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $username = trim($input['username'] ?? ''); $password = $input['password'] ?? ''; $nimi = trim($input['nimi'] ?? ''); $role = ($input['role'] ?? 'user') === 'admin' ? 'admin' : 'user'; if (empty($username) || empty($password)) { http_response_code(400); echo json_encode(['error' => 'Käyttäjätunnus ja salasana vaaditaan']); break; } if (strlen($password) < 4) { http_response_code(400); echo json_encode(['error' => 'Salasanan pitää olla vähintään 4 merkkiä']); break; } $users = loadUsers(); foreach ($users as $u) { if ($u['username'] === $username) { http_response_code(400); echo json_encode(['error' => 'Käyttäjätunnus on jo käytössä']); break 2; } } $newUser = [ 'id' => generateId(), 'username' => $username, 'password_hash' => password_hash($password, PASSWORD_DEFAULT), 'nimi' => $nimi ?: $username, 'role' => $role, 'luotu' => date('Y-m-d H:i:s'), ]; $users[] = $newUser; saveUsers($users); addLog('user_create', '', '', "Lisäsi käyttäjän: {$username} ({$role})"); unset($newUser['password_hash']); echo json_encode($newUser); break; case 'user_update': requireAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; $users = loadUsers(); $found = false; foreach ($users as &$u) { if ($u['id'] === $id) { if (isset($input['nimi'])) $u['nimi'] = trim($input['nimi']); if (isset($input['role'])) $u['role'] = $input['role'] === 'admin' ? 'admin' : 'user'; if (!empty($input['password'])) { $u['password_hash'] = password_hash($input['password'], PASSWORD_DEFAULT); } $found = true; addLog('user_update', '', '', "Muokkasi käyttäjää: {$u['username']}"); $safe = $u; unset($safe['password_hash']); echo json_encode($safe); break; } } unset($u); if (!$found) { http_response_code(404); echo json_encode(['error' => 'Käyttäjää ei löydy']); break; } saveUsers($users); break; case 'user_delete': requireAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; if ($id === $_SESSION['user_id']) { http_response_code(400); echo json_encode(['error' => 'Et voi poistaa itseäsi']); break; } $users = loadUsers(); $deleted = null; foreach ($users as $u) { if ($u['id'] === $id) { $deleted = $u; break; } } $users = array_values(array_filter($users, fn($u) => $u['id'] !== $id)); saveUsers($users); if ($deleted) addLog('user_delete', '', '', "Poisti käyttäjän: {$deleted['username']}"); echo json_encode(['success' => true]); break; // ---------- CHANGELOG ---------- case 'changelog': requireAuth(); $log = json_decode(file_get_contents(CHANGELOG_FILE), true) ?: []; $limit = intval($_GET['limit'] ?? 100); echo json_encode(array_slice($log, 0, $limit)); break; // ---------- CUSTOMERS ---------- case 'customers': requireAuth(); if ($method === 'GET') { echo json_encode(loadCustomers()); } break; case 'customer': requireAuth(); if ($method === 'POST') { $input = json_decode(file_get_contents('php://input'), true); $customers = loadCustomers(); $customer = [ 'id' => generateId(), 'yritys' => trim($input['yritys'] ?? ''), 'yhteyshenkilö' => trim($input['yhteyshenkilö'] ?? ''), 'puhelin' => trim($input['puhelin'] ?? ''), 'sahkoposti' => trim($input['sahkoposti'] ?? ''), 'laskutusosoite' => trim($input['laskutusosoite'] ?? ''), 'laskutuspostinumero' => trim($input['laskutuspostinumero'] ?? ''), 'laskutuskaupunki' => trim($input['laskutuskaupunki'] ?? ''), 'laskutussahkoposti' => trim($input['laskutussahkoposti'] ?? ''), 'elaskuosoite' => trim($input['elaskuosoite'] ?? ''), 'elaskuvalittaja' => trim($input['elaskuvalittaja'] ?? ''), 'ytunnus' => trim($input['ytunnus'] ?? ''), 'lisatiedot' => trim($input['lisatiedot'] ?? ''), 'liittymat' => parseLiittymat($input), 'luotu' => date('Y-m-d H:i:s'), ]; if (empty($customer['yritys'])) { http_response_code(400); echo json_encode(['error' => 'Yrityksen nimi vaaditaan']); break; } $customers[] = $customer; saveCustomers($customers); addLog('customer_create', $customer['id'], $customer['yritys'], 'Lisäsi asiakkaan'); echo json_encode($customer); } break; case 'customer_update': requireAuth(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; $customers = loadCustomers(); $found = false; foreach ($customers as &$c) { if ($c['id'] === $id) { $changes = []; $fields = ['yritys','yhteyshenkilö','puhelin','sahkoposti','laskutusosoite','laskutuspostinumero','laskutuskaupunki','laskutussahkoposti','elaskuosoite','elaskuvalittaja','ytunnus','lisatiedot']; foreach ($fields as $f) { if (isset($input[$f])) { $old = $c[$f] ?? ''; $new = trim($input[$f]); if ($old !== $new) $changes[] = $f; $c[$f] = $new; } } if (isset($input['liittymat'])) { $c['liittymat'] = parseLiittymat($input); $changes[] = 'liittymat'; } $c['muokattu'] = date('Y-m-d H:i:s'); $found = true; addLog('customer_update', $c['id'], $c['yritys'], 'Muokkasi: ' . implode(', ', $changes)); echo json_encode($c); break; } } unset($c); if (!$found) { http_response_code(404); echo json_encode(['error' => 'Asiakasta ei löydy']); break; } saveCustomers($customers); break; case 'customer_delete': requireAuth(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; $customers = loadCustomers(); $archived = null; $remaining = []; foreach ($customers as $c) { if ($c['id'] === $id) { $c['arkistoitu'] = date('Y-m-d H:i:s'); $c['arkistoija'] = currentUser(); $archived = $c; } else { $remaining[] = $c; } } if ($archived) { $archive = loadArchive(); $archive[] = $archived; saveArchive($archive); saveCustomers($remaining); addLog('customer_archive', $archived['id'], $archived['yritys'], 'Arkistoi asiakkaan'); } echo json_encode(['success' => true]); break; // ---------- ARCHIVE ---------- case 'archived_customers': requireAuth(); echo json_encode(loadArchive()); break; case 'customer_restore': requireAuth(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; $archive = loadArchive(); $restored = null; $remaining = []; foreach ($archive as $c) { if ($c['id'] === $id) { unset($c['arkistoitu'], $c['arkistoija']); $restored = $c; } else { $remaining[] = $c; } } if ($restored) { $customers = loadCustomers(); $customers[] = $restored; saveCustomers($customers); saveArchive($remaining); addLog('customer_restore', $restored['id'], $restored['yritys'], 'Palautti asiakkaan arkistosta'); } echo json_encode(['success' => true]); break; case 'customer_permanent_delete': requireAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; $archive = loadArchive(); $deleted = null; foreach ($archive as $c) { if ($c['id'] === $id) { $deleted = $c; break; } } $archive = array_values(array_filter($archive, fn($c) => $c['id'] !== $id)); saveArchive($archive); // Poista tiedostot $filesDir = DATA_DIR . '/files/' . $id; if (is_dir($filesDir)) { array_map('unlink', glob($filesDir . '/*')); rmdir($filesDir); } if ($deleted) addLog('customer_permanent_delete', $id, $deleted['yritys'] ?? '', 'Poisti pysyvästi'); echo json_encode(['success' => true]); break; // ---------- FILES ---------- case 'file_upload': requireAuth(); if ($method !== 'POST') break; $customerId = $_POST['customer_id'] ?? ''; if (!$customerId || !preg_match('/^[a-f0-9]+$/', $customerId)) { http_response_code(400); echo json_encode(['error' => 'Virheellinen asiakas-ID']); break; } if (empty($_FILES['file'])) { http_response_code(400); echo json_encode(['error' => 'Tiedosto puuttuu']); break; } $file = $_FILES['file']; if ($file['error'] !== UPLOAD_ERR_OK) { http_response_code(400); echo json_encode(['error' => 'Tiedoston lähetys epäonnistui']); break; } if ($file['size'] > 20 * 1024 * 1024) { http_response_code(400); echo json_encode(['error' => 'Tiedosto on liian suuri (max 20 MB)']); break; } $uploadDir = DATA_DIR . '/files/' . $customerId; if (!file_exists($uploadDir)) mkdir($uploadDir, 0755, true); $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($file['name'])); $dest = $uploadDir . '/' . $safeName; if (file_exists($dest)) { $ext = pathinfo($safeName, PATHINFO_EXTENSION); $base = pathinfo($safeName, PATHINFO_FILENAME); $safeName = $base . '_' . date('His') . ($ext ? '.' . $ext : ''); $dest = $uploadDir . '/' . $safeName; } if (move_uploaded_file($file['tmp_name'], $dest)) { echo json_encode(['success' => true, 'filename' => $safeName, 'size' => $file['size']]); } else { http_response_code(500); echo json_encode(['error' => 'Tallennusvirhe']); } break; case 'file_list': requireAuth(); $customerId = $_GET['customer_id'] ?? ''; if (!$customerId || !preg_match('/^[a-f0-9]+$/', $customerId)) { echo json_encode([]); break; } $dir = DATA_DIR . '/files/' . $customerId; $files = []; if (is_dir($dir)) { foreach (scandir($dir) as $f) { if ($f === '.' || $f === '..') continue; $path = $dir . '/' . $f; $files[] = ['filename' => $f, 'size' => filesize($path), 'modified' => date('Y-m-d H:i', filemtime($path))]; } } usort($files, fn($a, $b) => strcmp($b['modified'], $a['modified'])); echo json_encode($files); break; case 'file_download': requireAuth(); $customerId = $_GET['customer_id'] ?? ''; $filename = $_GET['filename'] ?? ''; if (!$customerId || !preg_match('/^[a-f0-9]+$/', $customerId) || !$filename) { http_response_code(400); echo json_encode(['error' => 'Virheelliset parametrit']); break; } $safeName = basename($filename); $path = DATA_DIR . '/files/' . $customerId . '/' . $safeName; if (!file_exists($path)) { http_response_code(404); echo json_encode(['error' => 'Tiedostoa ei löydy']); break; } header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="' . $safeName . '"'); header('Content-Length: ' . filesize($path)); readfile($path); exit; case 'file_delete': requireAuth(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $customerId = $input['customer_id'] ?? ''; $filename = $input['filename'] ?? ''; if (!$customerId || !preg_match('/^[a-f0-9]+$/', $customerId) || !$filename) { http_response_code(400); echo json_encode(['error' => 'Virheelliset parametrit']); break; } $safeName = basename($filename); $path = DATA_DIR . '/files/' . $customerId . '/' . $safeName; if (file_exists($path)) unlink($path); echo json_encode(['success' => true]); break; default: http_response_code(404); echo json_encode(['error' => 'Tuntematon toiminto']); break; }