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.';