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 @@
-