diff --git a/.gitignore b/.gitignore index 66e397b..67812ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ data/customers.json +data/users.json +data/changelog.json +data/archive.json data/backups/ data/files/ diff --git a/api.php b/api.php index c81c91b..1330c0a 100644 --- a/api.php +++ b/api.php @@ -2,33 +2,99 @@ session_start(); header('Content-Type: application/json'); -define('ADMIN_PASSWORD', 'cuitunet2024'); -define('DATA_FILE', __DIR__ . '/data/customers.json'); +define('DATA_DIR', __DIR__ . '/data'); +define('DATA_FILE', DATA_DIR . '/customers.json'); +define('USERS_FILE', DATA_DIR . '/users.json'); +define('CHANGELOG_FILE', DATA_DIR . '/changelog.json'); +define('ARCHIVE_FILE', DATA_DIR . '/archive.json'); -// Varmista data-kansio -if (!file_exists(__DIR__ . '/data')) { - mkdir(__DIR__ . '/data', 0755, true); -} -if (!file_exists(DATA_FILE)) { - file_put_contents(DATA_FILE, '[]'); +// Varmista data-kansio ja tiedostot +if (!file_exists(DATA_DIR)) mkdir(DATA_DIR, 0755, true); +foreach ([DATA_FILE, USERS_FILE, CHANGELOG_FILE, ARCHIVE_FILE] as $f) { + if (!file_exists($f)) file_put_contents($f, '[]'); } +// Luo oletuskäyttäjä jos users.json on tyhjä +initUsers(); + $method = $_SERVER['REQUEST_METHOD']; $action = $_GET['action'] ?? ''; -// Auth-tarkistus (paitsi login) +// ==================== HELPERS ==================== + function requireAuth() { - if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + if (!isset($_SESSION['user_id'])) { http_response_code(401); echo json_encode(['error' => '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) ?: []; - // Migroi vanha data: jos asiakkaalla ei ole liittymat-arrayta, luo se $migrated = false; foreach ($customers as &$c) { if (!isset($c['liittymat'])) { @@ -53,12 +119,10 @@ function loadCustomers(): array { } function saveCustomers(array $customers): void { - // Automaattinen backup ennen tallennusta if (file_exists(DATA_FILE) && filesize(DATA_FILE) > 2) { - $backupDir = __DIR__ . '/data/backups'; + $backupDir = DATA_DIR . '/backups'; if (!file_exists($backupDir)) mkdir($backupDir, 0755, true); copy(DATA_FILE, $backupDir . '/customers_' . date('Y-m-d_His') . '.json'); - // Säilytä vain 30 viimeisintä backuppia $backups = glob($backupDir . '/customers_*.json'); if (count($backups) > 30) { sort($backups); @@ -68,21 +132,61 @@ function saveCustomers(array $customers): void { file_put_contents(DATA_FILE, json_encode($customers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } -function generateId(): string { - return bin2hex(random_bytes(8)); +// ==================== 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'] ?? ''; - if ($password === ADMIN_PASSWORD) { - $_SESSION['authenticated'] = true; - echo json_encode(['success' => true]); - } else { + $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ä salasana']); + echo json_encode(['error' => 'Väärä käyttäjätunnus tai salasana']); } break; @@ -92,14 +196,136 @@ switch ($action) { break; case 'check_auth': - echo json_encode(['authenticated' => isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true]); + 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') { - $customers = loadCustomers(); - echo json_encode($customers); + echo json_encode(loadCustomers()); } break; @@ -108,21 +334,6 @@ switch ($action) { if ($method === 'POST') { $input = json_decode(file_get_contents('php://input'), true); $customers = loadCustomers(); - $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' => '']; - } $customer = [ 'id' => generateId(), 'yritys' => trim($input['yritys'] ?? ''), @@ -137,7 +348,7 @@ switch ($action) { 'elaskuvalittaja' => trim($input['elaskuvalittaja'] ?? ''), 'ytunnus' => trim($input['ytunnus'] ?? ''), 'lisatiedot' => trim($input['lisatiedot'] ?? ''), - 'liittymat' => $liittymat, + 'liittymat' => parseLiittymat($input), 'luotu' => date('Y-m-d H:i:s'), ]; if (empty($customer['yritys'])) { @@ -147,6 +358,7 @@ switch ($action) { } $customers[] = $customer; saveCustomers($customers); + addLog('customer_create', $customer['id'], $customer['yritys'], 'Lisäsi asiakkaan'); echo json_encode($customer); } break; @@ -160,35 +372,23 @@ switch ($action) { $found = false; foreach ($customers as &$c) { if ($c['id'] === $id) { - $c['yritys'] = trim($input['yritys'] ?? $c['yritys']); - $c['yhteyshenkilö'] = trim($input['yhteyshenkilö'] ?? $c['yhteyshenkilö']); - $c['puhelin'] = trim($input['puhelin'] ?? $c['puhelin']); - $c['sahkoposti'] = trim($input['sahkoposti'] ?? $c['sahkoposti']); - $c['laskutusosoite'] = trim($input['laskutusosoite'] ?? $c['laskutusosoite']); - $c['laskutuspostinumero'] = trim($input['laskutuspostinumero'] ?? ($c['laskutuspostinumero'] ?? '')); - $c['laskutuskaupunki'] = trim($input['laskutuskaupunki'] ?? ($c['laskutuskaupunki'] ?? '')); - $c['laskutussahkoposti'] = trim($input['laskutussahkoposti'] ?? $c['laskutussahkoposti']); - $c['elaskuosoite'] = trim($input['elaskuosoite'] ?? ($c['elaskuosoite'] ?? '')); - $c['elaskuvalittaja'] = trim($input['elaskuvalittaja'] ?? ($c['elaskuvalittaja'] ?? '')); - $c['ytunnus'] = trim($input['ytunnus'] ?? $c['ytunnus']); - $c['lisatiedot'] = trim($input['lisatiedot'] ?? $c['lisatiedot']); - if (isset($input['liittymat'])) { - $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'] ?? ''), - ]; + $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; } - $c['liittymat'] = $liittymat; + } + 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; } @@ -202,6 +402,88 @@ switch ($action) { 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; @@ -222,16 +504,14 @@ switch ($action) { echo json_encode(['error' => 'Tiedoston lähetys epäonnistui']); break; } - // Max 20MB if ($file['size'] > 20 * 1024 * 1024) { http_response_code(400); echo json_encode(['error' => 'Tiedosto on liian suuri (max 20 MB)']); break; } - $uploadDir = __DIR__ . '/data/files/' . $customerId; + $uploadDir = DATA_DIR . '/files/' . $customerId; if (!file_exists($uploadDir)) mkdir($uploadDir, 0755, true); $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($file['name'])); - // Jos samanniminen tiedosto on jo olemassa, lisää aikaleima $dest = $uploadDir . '/' . $safeName; if (file_exists($dest)) { $ext = pathinfo($safeName, PATHINFO_EXTENSION); @@ -240,11 +520,7 @@ switch ($action) { $dest = $uploadDir . '/' . $safeName; } if (move_uploaded_file($file['tmp_name'], $dest)) { - echo json_encode([ - 'success' => true, - 'filename' => $safeName, - 'size' => $file['size'], - ]); + echo json_encode(['success' => true, 'filename' => $safeName, 'size' => $file['size']]); } else { http_response_code(500); echo json_encode(['error' => 'Tallennusvirhe']); @@ -258,17 +534,13 @@ switch ($action) { echo json_encode([]); break; } - $dir = __DIR__ . '/data/files/' . $customerId; + $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)), - ]; + $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'])); @@ -285,7 +557,7 @@ switch ($action) { break; } $safeName = basename($filename); - $path = __DIR__ . '/data/files/' . $customerId . '/' . $safeName; + $path = DATA_DIR . '/files/' . $customerId . '/' . $safeName; if (!file_exists($path)) { http_response_code(404); echo json_encode(['error' => 'Tiedostoa ei löydy']); @@ -309,27 +581,8 @@ switch ($action) { break; } $safeName = basename($filename); - $path = __DIR__ . '/data/files/' . $customerId . '/' . $safeName; - if (file_exists($path)) { - unlink($path); - } - echo json_encode(['success' => true]); - break; - - case 'customer_delete': - requireAuth(); - if ($method !== 'POST') break; - $input = json_decode(file_get_contents('php://input'), true); - $id = $input['id'] ?? ''; - $customers = loadCustomers(); - $customers = array_values(array_filter($customers, fn($c) => $c['id'] !== $id)); - saveCustomers($customers); - // Poista asiakkaan tiedostot - $filesDir = __DIR__ . '/data/files/' . $id; - if (is_dir($filesDir)) { - array_map('unlink', glob($filesDir . '/*')); - rmdir($filesDir); - } + $path = DATA_DIR . '/files/' . $customerId . '/' . $safeName; + if (file_exists($path)) unlink($path); echo json_encode(['success' => true]); break; diff --git a/index.html b/index.html index 0e3f1bd..7f8d8e3 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,8 @@

CuituNet Intra

Kirjaudu sisään

- + +
@@ -33,85 +34,160 @@
+
-
-
- -
- -
- + + +
+
+
+ +
+
+ + + + + + + + + + + +
KäyttäjätunnusNimiRooliLuotuToiminnot
+
@@ -129,7 +205,6 @@
-

Perustiedot

@@ -141,10 +216,8 @@
-

Liittymät

-

Yhteystiedot

@@ -160,7 +233,6 @@
-

Laskutustiedot

-

Lisätiedot

-
@@ -212,7 +282,7 @@
- +