From 5850b97c54aaf39177b13463840387215187718f Mon Sep 17 00:00:00 2001 From: Steffen Pohle Date: Fri, 31 Jul 2026 19:01:44 +0200 Subject: [PATCH] seperated user files and audit logs. --- auth.php | 321 +++++++++++++++++++++++++++++++++++++++++++------- chat.php | 43 ++++++- config.php | 40 +------ default.php | 7 +- index.php | 4 +- login.php | 16 ++- savechat.php | 195 ++++++++++-------------------- useradmin.php | 5 +- 8 files changed, 410 insertions(+), 221 deletions(-) diff --git a/auth.php b/auth.php index 30978c9..b404b1e 100644 --- a/auth.php +++ b/auth.php @@ -4,65 +4,208 @@ if (session_status() === PHP_SESSION_NONE) { session_start(); } -function ensureConfigDirectory() { - if (!is_dir(CONFIG_PATH)) { - if (!mkdir(CONFIG_PATH, 0755, true) && !is_dir(CONFIG_PATH)) { - throw new RuntimeException('Unable to create config directory.'); - } +require_once __DIR__ . '/default.php'; + +function getBrowserIdentifier() { + $agent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'; + if (preg_match('/(Edg|Chrome|Chromium|Firefox|Safari|Opera|MSIE|Trident)/i', $agent, $matches)) { + return trim($matches[1]); } + return 'unknown'; } -function getConfigData() { - ensureConfigDirectory(); +function getRememberMeCookieName($username) { + return 'kai_remember_' . sanitizeUsername($username); +} - if (!file_exists(CONFIG_FILE)) { - return ['defaultModel' => '', 'users' => []]; +function createRememberMeToken($username) { + $browser = getBrowserIdentifier(); + $randomValue = bin2hex(random_bytes(16)); + $token = hash_hmac('sha256', $username . '|' . $browser . '|' . $randomValue, APP_SECRET); + + return [ + 'token' => $token, + 'browser' => $browser, + 'createdAt' => time(), + 'lastUsedAt' => time() + ]; +} + +function setRememberMeCookie($username) { + $username = trim((string) $username); + if ($username === '') { + return false; } - $content = @file_get_contents(CONFIG_FILE); - if ($content === false) { - return ['defaultModel' => '', 'users' => []]; + $tokenData = createRememberMeToken($username); + $config = getUserConfig($username); + $tokens = is_array($config['rememberMeTokens'] ?? null) ? $config['rememberMeTokens'] : []; + $tokens[] = $tokenData; + + if (count($tokens) > 10) { + usort($tokens, function ($a, $b) { + return ($a['lastUsedAt'] ?? 0) <=> ($b['lastUsedAt'] ?? 0); + }); + $tokens = array_slice($tokens, -10); } - $data = json_decode($content, true); - if (!is_array($data)) { - return ['defaultModel' => '', 'users' => []]; + $config['rememberMeTokens'] = $tokens; + if (!saveUserConfigData($username, $config)) { + return false; } - if (!isset($data['users']) || !is_array($data['users'])) { - $data['users'] = []; + $cookieName = getRememberMeCookieName($username); + return setcookie($cookieName, $tokenData['token'], time() + 60 * 60 * 24 * 30, '/', '', false, true); +} + +function clearRememberMeCookie($username) { + $username = trim((string) $username); + if ($username === '') { + return; } - return $data; + $cookieName = getRememberMeCookieName($username); + setcookie($cookieName, '', time() - 3600, '/', '', false, true); } -function saveConfigData($config) { +function tryRememberMeLogin() { + if (!empty($_SESSION['auth_user']['username'])) { + return true; + } + + foreach ($_COOKIE as $cookieName => $cookieValue) { + if (strpos($cookieName, 'kai_remember_') !== 0) { + continue; + } + + $username = sanitizeUsername(substr($cookieName, strlen('kai_remember_'))); + if ($username === '') { + continue; + } + + $user = findUserByUsername($username); + if ($user === null) { + continue; + } + + $config = getUserConfig($username); + $tokens = is_array($config['rememberMeTokens'] ?? null) ? $config['rememberMeTokens'] : []; + $browser = getBrowserIdentifier(); + foreach ($tokens as &$entry) { + if (($entry['token'] ?? '') !== $cookieValue) { + continue; + } + if (!empty($entry['browser']) && $entry['browser'] !== $browser) { + continue; + } + + $entry['lastUsedAt'] = time(); + $config['rememberMeTokens'] = $tokens; + saveUserConfigData($username, $config); + + $_SESSION['auth_user'] = [ + 'username' => $user['username'], + 'role' => $user['role'] ?? 'user' + ]; + return true; + } + } + + return false; +} + +function ensureConfigDirectory() { + if (!is_dir(USER_CONFIG_DIR)) { + if (!mkdir(USER_CONFIG_DIR, 0755, true) && !is_dir(USER_CONFIG_DIR)) { + throw new RuntimeException('Unable to create config directory.'); + } + } +} + +function sanitizeUsername($username) { + $username = trim((string) $username); + $username = preg_replace('/[^A-Za-z0-9._-]/', '', $username); + return $username !== '' ? $username : 'user'; +} + +function getUserConfigPath($username) { + return USER_CONFIG_DIR . '/' . sanitizeUsername($username) . '-config.php'; +} + +function getUserChatPath($username) { + return USER_CONFIG_DIR . '/' . sanitizeUsername($username) . '-chat.php'; +} + +function readUserConfig($username) { + $path = getUserConfigPath($username); + if (!file_exists($path)) { + return []; + } + + $data = include $path; + return is_array($data) ? $data : []; +} + +function writeUserConfig($username, $config) { ensureConfigDirectory(); - $data = getConfigData(); - if (array_key_exists('defaultModel', $config)) { - $data['defaultModel'] = (string) $config['defaultModel']; + $username = trim((string) $username); + if ($username === '') { + return false; } - if (array_key_exists('users', $config)) { - $data['users'] = is_array($config['users']) ? $config['users'] : []; + $data = is_array($config) ? $config : []; + $data['username'] = $username; + $data['role'] = (string) ($data['role'] ?? 'user'); + $data['email'] = (string) ($data['email'] ?? ''); + $data['passwordHash'] = (string) ($data['passwordHash'] ?? ''); + $data['defaultModel'] = (string) ($data['defaultModel'] ?? ''); + + $php = " isset($data['defaultModel']) ? (string) $data['defaultModel'] : '', - 'users' => isset($data['users']) && is_array($data['users']) ? $data['users'] : [] - ]; + $username = trim((string) $username); + if ($username === '') { + return false; + } + + $php = " $users]); +function saveUsers($users, $previousUsers = null) { + ensureConfigDirectory(); + + $normalizedUsers = []; + foreach ($users as $index => $user) { + $entry = is_array($user) ? $user : []; + $username = trim((string) ($entry['username'] ?? '')); + if ($username === '') { + continue; + } + + $entry['username'] = $username; + $entry['role'] = (string) ($entry['role'] ?? 'user'); + $entry['email'] = (string) ($entry['email'] ?? ''); + $entry['passwordHash'] = (string) ($entry['passwordHash'] ?? ''); + $entry['defaultModel'] = (string) ($entry['defaultModel'] ?? ''); + $normalizedUsers[] = $entry; + } + + $previousUsers = is_array($previousUsers) ? $previousUsers : loadUsers(); + foreach ($normalizedUsers as $index => $entry) { + $oldUsername = isset($previousUsers[$index]) ? trim((string) ($previousUsers[$index]['username'] ?? '')) : ''; + $newUsername = trim((string) ($entry['username'] ?? '')); + if ($oldUsername !== '' && $oldUsername !== $newUsername) { + renameUserFiles($oldUsername, $newUsername); + } + + if (!writeUserConfig($newUsername, $entry)) { + return false; + } + } + + $currentUsernames = []; + foreach ($normalizedUsers as $entry) { + $currentUsernames[] = sanitizeUsername($entry['username']); + } + + $files = glob(USER_CONFIG_DIR . '/*-config.php'); + if ($files !== false) { + foreach ($files as $file) { + $name = basename($file, '-config.php'); + if (!in_array($name, $currentUsernames, true)) { + @unlink(USER_CONFIG_DIR . '/' . $name . '-config.php'); + @unlink(USER_CONFIG_DIR . '/' . $name . '-chat.php'); + } + } + } + + return true; } function findUserByUsername($username) { @@ -132,9 +354,14 @@ function createUser($username, $password, $role = 'user', $email = '') { 'username' => $username, 'email' => $email, 'passwordHash' => hashPassword($password), - 'role' => $role + 'role' => $role, + 'defaultModel' => '' ]; + if (!writeUserChat($username, [])) { + return false; + } + return saveUsers($users); } @@ -222,7 +449,16 @@ function getAuthenticatedUser() { return null; } - return $_SESSION['auth_user']; + return findUserByUsername($_SESSION['auth_user']['username'] ?? ''); +} + +function getAuthenticatedUsername() { + $user = getAuthenticatedUser(); + if ($user !== null) { + return (string) ($user['username'] ?? ''); + } + + return (string) ($_SESSION['auth_user']['username'] ?? ''); } function requireAuthentication() { @@ -243,5 +479,10 @@ function requireAdmin() { } function logoutUser() { + $username = trim((string) ($_SESSION['auth_user']['username'] ?? '')); unset($_SESSION['auth_user']); + + if ($username !== '') { + clearRememberMeCookie($username); + } } diff --git a/chat.php b/chat.php index 663f9e9..334ffd0 100644 --- a/chat.php +++ b/chat.php @@ -2,6 +2,35 @@ header('Content-Type: application/json'); require_once __DIR__ . '/default.php'; +require_once __DIR__ . '/auth.php'; + +function appendChatAuditEntry($username, $chatMessages, $resultPayload, $ipAddress = null) { + ensureConfigDirectory(); + + $auditPath = USER_CONFIG_DIR . '/chat-requests.json'; + $entries = []; + + if (file_exists($auditPath)) { + $raw = file_get_contents($auditPath); + if ($raw !== false) { + $decoded = json_decode($raw, true); + if (is_array($decoded)) { + $entries = $decoded; + } + } + } + + $entries[] = [ + 'timestamp' => date('c'), + 'username' => (string) $username, + 'ipAddress' => (string) ($ipAddress ?? ($_SERVER['REMOTE_ADDR'] ?? '')), + 'chat' => is_array($chatMessages) ? $chatMessages : [$chatMessages], + 'result' => $resultPayload + ]; + + $payload = json_encode($entries, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + return $payload !== false && file_put_contents($auditPath, $payload . PHP_EOL) !== false; +} // 1. Handle fetching available models if (isset($_GET['action']) && $_GET['action'] === 'models') { @@ -24,6 +53,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $model = $input['model'] ?? 'llama3'; $messages = $input['messages'] ?? []; + $username = ''; + if (!empty($_SESSION['auth_user']['username'])) { + $username = sanitizeUsername($_SESSION['auth_user']['username']); + } $payload = json_encode([ 'model' => $model, @@ -38,13 +71,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); $response = curl_exec($ch); - + $resultPayload = null; + if (curl_errno($ch)) { - echo json_encode(['error' => 'Error communicating with remote Ollama: ' . curl_error($ch)]); + $resultPayload = ['error' => 'Error communicating with remote Ollama: ' . curl_error($ch)]; + echo json_encode($resultPayload); } else { + $decodedResponse = json_decode($response, true); + $resultPayload = $decodedResponse !== null ? $decodedResponse : $response; echo $response; } curl_close($ch); + + appendChatAuditEntry($username, $messages, $resultPayload, $_SERVER['REMOTE_ADDR'] ?? ''); exit; } diff --git a/config.php b/config.php index ca4e7d1..2d319ca 100644 --- a/config.php +++ b/config.php @@ -1,40 +1,4 @@ 'POST required.']); - exit; - } - - $input = json_decode(file_get_contents('php://input'), true); - if (!is_array($input)) { - echo json_encode(['error' => 'Invalid JSON.']); - exit; - } - - $config = readConfig(); - $config['defaultModel'] = isset($input['defaultModel']) ? (string) $input['defaultModel'] : ''; - - if (!writeConfig($config)) { - echo json_encode(['error' => 'Unable to save config.']); - exit; - } - - echo json_encode(['success' => true, 'defaultModel' => $config['defaultModel']]); - exit; - - default: - echo json_encode(['error' => 'Unknown action.']); - exit; -} +define('USER_CONFIG_DIR', '/var/lib/KaI/users'); +define('OLLAMA_URL', 'http://127.0.0.1:11434'); diff --git a/default.php b/default.php index 3584794..74943cc 100644 --- a/default.php +++ b/default.php @@ -1,10 +1,9 @@ $user['username'], 'role' => $user['role'] ?? 'user' ]; + + if (!empty($_POST['remember'])) { + setRememberMeCookie($user['username']); + } else { + clearRememberMeCookie($user['username']); + } + header('Location: index.php'); exit; } @@ -36,9 +43,7 @@ if (isLoggedIn()) { exit; } -if (!file_exists(CONFIG_FILE)) { - ensureConfigDirectory(); -} +ensureConfigDirectory(); if (empty(loadUsers())) { $created = createUser('admin', 'changeme', 'admin', 'admin@example.com'); @@ -75,6 +80,11 @@ if (empty(loadUsers())) { + + diff --git a/savechat.php b/savechat.php index d1b8b56..1db7a79 100644 --- a/savechat.php +++ b/savechat.php @@ -2,169 +2,118 @@ header('Content-Type: application/json'); require_once __DIR__ . '/default.php'; +require_once __DIR__ . '/auth.php'; -/*---------------------------------------------------------- - - Create storage file if necessary - ----------------------------------------------------------*/ -if (!file_exists(CHAT_HISTORY_FILE)) { - if (@file_put_contents(CHAT_HISTORY_FILE, '') === false) { - http_response_code(500); - echo json_encode([ - 'error' => 'Cannot create history file.' - ]); - exit; - } -} - -/*---------------------------------------------------------- - - Helper - ----------------------------------------------------------*/ -function readChats() { - $result = []; - - $fp = fopen(CHAT_HISTORY_FILE, 'c+'); - if (!$fp) { - return []; - } - - flock($fp, LOCK_SH); - while (($line = fgets($fp)) !== false) { - - $line = trim($line); - if ($line === '') { - continue; - } - - $obj = json_decode($line, true); - if (is_array($obj)) { - $result[] = $obj; - } - } +requireAuthentication(); - flock($fp, LOCK_UN); - fclose($fp); - - return $result; -} - -function writeChats($chats) { - $fp = fopen(CHAT_HISTORY_FILE, 'w'); - - if (!$fp) { - return false; - } - - flock($fp, LOCK_EX); - foreach ($chats as $chat) { - fwrite($fp, json_encode($chat, JSON_UNESCAPED_UNICODE) . PHP_EOL); - } - - fflush($fp); - - flock($fp, LOCK_UN); - fclose($fp); - - return true; -} +$username = getAuthenticatedUsername(); function createTitle($messages) { foreach ($messages as $msg) { if (($msg['role'] ?? '') === 'user') { $title = trim($msg['content']); if (mb_strlen($title) > 60) { - $title = mb_substr($title, 0, 60) . "..."; + $title = mb_substr($title, 0, 60) . '...'; } return $title; } } - return "Untitled Chat"; + return 'Untitled Chat'; } -/*---------------------------------------------------------- - - Action - ----------------------------------------------------------*/ - $action = $_GET['action'] ?? ''; switch ($action) { - /*------------------------------------------------------ - - LIST - ------------------------------------------------------*/ + case 'load-config': + $config = getUserConfig($username); + echo json_encode([ + 'defaultModel' => (string) ($config['defaultModel'] ?? '') + ]); + exit; + + case 'save-config': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + echo json_encode(['error' => 'POST required.']); + exit; + } + + $input = json_decode(file_get_contents('php://input'), true); + if (!is_array($input)) { + echo json_encode(['error' => 'Invalid JSON.']); + exit; + } + + $config = getUserConfig($username); + $config['defaultModel'] = isset($input['defaultModel']) ? (string) $input['defaultModel'] : ''; + + if (!saveUserConfigData($username, $config)) { + echo json_encode(['error' => 'Unable to save config.']); + exit; + } + + echo json_encode(['success' => true, 'defaultModel' => $config['defaultModel']]); + exit; + case 'list': - $chats = readChats(); + $chats = readUserChat($username); $list = []; foreach ($chats as $chat) { $list[] = [ - 'id' => $chat['id'], - 'title' => $chat['title'], - 'created' => $chat['created'], + 'id' => $chat['id'] ?? '', + 'title' => $chat['title'] ?? 'Untitled Chat', + 'created' => $chat['created'] ?? '', 'model' => $chat['model'] ?? '' ]; } - usort($list, function($a, $b) { + usort($list, function ($a, $b) { return strcmp($b['created'], $a['created']); }); echo json_encode($list); exit; - /*------------------------------------------------------ - - LOAD - ------------------------------------------------------*/ case 'load': $id = $_GET['id'] ?? ''; - if ($id == '') { - echo json_encode([ - 'error' => 'Missing id.' - ]); + if ($id === '') { + echo json_encode(['error' => 'Missing id.']); exit; } - $chats = readChats(); + $chats = readUserChat($username); foreach ($chats as $chat) { - if ($chat['id'] === $id) { + if (($chat['id'] ?? '') === $id) { echo json_encode($chat); exit; } } - echo json_encode([ - 'error' => 'Chat not found.' - ]); + echo json_encode(['error' => 'Chat not found.']); exit; - /*------------------------------------------------------ - - SAVE - ------------------------------------------------------*/ + case 'save': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - echo json_encode([ - 'error' => 'POST required.' - ]); + echo json_encode(['error' => 'POST required.']); exit; } $input = json_decode(file_get_contents('php://input'), true); if (!is_array($input)) { - echo json_encode([ - 'error' => 'Invalid JSON.' - ]); + echo json_encode(['error' => 'Invalid JSON.']); exit; } $messages = $input['messages'] ?? []; - if (!is_array($messages) || count($messages) == 0) { - echo json_encode([ - 'error' => 'No messages.' - ]); + if (!is_array($messages) || count($messages) === 0) { + echo json_encode(['error' => 'No messages.']); exit; } $id = $input['id'] ?? ''; - if ($id == '') { + if ($id === '') { $id = uniqid('', true); } $title = createTitle($messages); - $model = $input['model'] ?? ''; + $model = isset($input['model']) ? (string) $input['model'] : ''; $chat = [ 'id' => $id, 'title' => $title, @@ -173,10 +122,10 @@ switch ($action) { 'messages' => $messages ]; - $chats = readChats(); + $chats = readUserChat($username); $found = false; foreach ($chats as &$existing) { - if ($existing['id'] === $id) { + if (($existing['id'] ?? '') === $id) { $existing = $chat; $found = true; break; @@ -187,10 +136,10 @@ switch ($action) { $chats[] = $chat; } - if (!writeChats($chats)) { - echo json_encode([ - 'error' => 'Unable to save.' - ]); + $config = getUserConfig($username); + $config['defaultModel'] = $model; + if (!saveUserConfigData($username, $config) || !writeUserChat($username, $chats)) { + echo json_encode(['error' => 'Unable to save.']); exit; } @@ -201,40 +150,26 @@ switch ($action) { ]); exit; - /*------------------------------------------------------ - - DELETE - ------------------------------------------------------*/ case 'delete': $id = $_GET['id'] ?? ''; - - if ($id == '') { - echo json_encode([ - 'error' => 'Missing id.' - ]); + if ($id === '') { + echo json_encode(['error' => 'Missing id.']); exit; } - $chats = readChats(); + $chats = readUserChat($username); $newChats = []; - foreach ($chats as $chat) { - if ($chat['id'] !== $id) { + if (($chat['id'] ?? '') !== $id) { $newChats[] = $chat; } } - writeChats($newChats); - echo json_encode([ - 'success' => true - ]); + writeUserChat($username, $newChats); + echo json_encode(['success' => true]); exit; - /*------------------------------------------------------ - - DEFAULT - ------------------------------------------------------*/ default: - echo json_encode([ - 'error' => 'Unknown action.' - ]); + echo json_encode(['error' => 'Unknown action.']); exit; } diff --git a/useradmin.php b/useradmin.php index 55ff182..7c9912f 100644 --- a/useradmin.php +++ b/useradmin.php @@ -11,6 +11,7 @@ requireAuthentication(); requireAdmin(); $users = loadUsers(); +$previousUsers = $users; $errors = []; $success = ''; @@ -49,7 +50,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $users[$index]['passwordHash'] = hashPassword($password); } $users[$index]['role'] = $role; - if (saveUsers($users)) { + if (saveUsers($users, $previousUsers)) { $success = 'User updated successfully.'; } else { $errors[] = 'Unable to update user.'; @@ -62,7 +63,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($index >= 0 && isset($users[$index])) { unset($users[$index]); $users = array_values($users); - if (saveUsers($users)) { + if (saveUsers($users, $previousUsers)) { $success = 'User deleted successfully.'; } else { $errors[] = 'Unable to delete user.';