Files
intra.noxus.fi/api.php
Jukka Lampikoski e4914e9edb Add user management, change log and customer archive
- Multi-user auth with username/password (replaces single password)
- Default admin account created automatically (admin/cuitunet2024)
- User CRUD with admin/user roles (only admin can manage users)
- All customer changes logged with timestamp, user and details
- Customer deletion now archives instead of permanently removing
- Archive view with restore and permanent delete options
- Tab navigation: Asiakkaat, Arkisto, Muutosloki, Käyttäjät
- Protect users.json, changelog.json and archive.json in .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:41:40 +02:00

594 lines
21 KiB
PHP

<?php
session_start();
header('Content-Type: application/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 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'] ?? '';
// ==================== HELPERS ====================
function requireAuth() {
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) ?: [];
$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;
}