getGuestUsername(), 'role' => 'guest', 'guest' => true, 'email' => '', 'passwordHash' => '' ]; } function loginAsGuest() { if (!isGuestModeEnabled()) { return false; } $_SESSION['auth_user'] = [ 'username' => getGuestUsername(), 'role' => 'guest' ]; return true; } function getRememberMeCookieName($username) { return 'kai_remember_' . sanitizeUsername($username); } 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; } $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); } $config['rememberMeTokens'] = $tokens; if (!saveUserConfigData($username, $config)) { return false; } $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; } $cookieName = getRememberMeCookieName($username); setcookie($cookieName, '', time() - 3600, '/', '', false, true); } 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.json'; } function getUserChatPath($username) { return USER_CONFIG_DIR . '/' . sanitizeUsername($username) . '-chat.json'; } function getConfiguredDefaultModel() { $defaultModel = defined('DEFAULT_MODEL') ? trim((string) DEFAULT_MODEL) : ''; return $defaultModel !== '' ? $defaultModel : 'llama3'; } function getDefaultModelForUser($username) { $config = getUserConfig($username); $storedModel = trim((string) ($config['defaultModel'] ?? '')); return $storedModel !== '' ? $storedModel : getConfiguredDefaultModel(); } function readUserConfig($username) { $path = getUserConfigPath($username); if (!file_exists($path)) { // Try migrating from legacy PHP file if present $legacy = USER_CONFIG_DIR . '/' . sanitizeUsername($username) . '-config.php'; if (file_exists($legacy)) { $raw = @file_get_contents($legacy); if ($raw !== false) { // attempt to include safely $data = @include $legacy; if (is_array($data)) { // write JSON migration writeUserConfig($username, $data); return $data; } } } return []; } $raw = @file_get_contents($path); if ($raw === false) { return []; } $decoded = json_decode($raw, true); return is_array($decoded) ? $decoded : []; } function writeUserConfig($username, $config) { ensureConfigDirectory(); $username = trim((string) $username); if ($username === '') { return false; } $data = is_array($config) ? $config : []; $data['username'] = $username; $data['role'] = (string) ($data['role'] ?? 'user'); $data['email'] = (string) ($data['email'] ?? ''); $data['passwordHash'] = (string) ($data['passwordHash'] ?? ''); $storedDefaultModel = trim((string) ($data['defaultModel'] ?? '')); $data['defaultModel'] = $storedDefaultModel !== '' ? $storedDefaultModel : getConfiguredDefaultModel(); $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if ($json === false) { return false; } $path = getUserConfigPath($username); $tmp = $path . '.tmp'; if (@file_put_contents($tmp, $json . "\n") === false) { return false; } return @rename($tmp, $path); } function readUserChat($username) { $path = getUserChatPath($username); if (!file_exists($path)) { // Try migrating legacy PHP chat file $legacy = USER_CONFIG_DIR . '/' . sanitizeUsername($username) . '-chat.php'; if (file_exists($legacy)) { $data = @include $legacy; if (is_array($data)) { writeUserChat($username, $data); return $data; } } return []; } $raw = @file_get_contents($path); if ($raw === false) { return []; } $decoded = json_decode($raw, true); return is_array($decoded) ? $decoded : []; } function writeUserChat($username, $chats) { ensureConfigDirectory(); $username = trim((string) $username); if ($username === '') { return false; } $json = json_encode(is_array($chats) ? $chats : [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if ($json === false) { return false; } $path = getUserChatPath($username); $tmp = $path . '.tmp'; if (@file_put_contents($tmp, $json . "\n") === false) { return false; } return @rename($tmp, $path); } function renameUserFiles($oldUsername, $newUsername) { $oldUsername = trim((string) $oldUsername); $newUsername = trim((string) $newUsername); if ($oldUsername === '' || $newUsername === '' || $oldUsername === $newUsername) { return; } $oldConfigPath = getUserConfigPath($oldUsername); $newConfigPath = getUserConfigPath($newUsername); if (file_exists($oldConfigPath) && !file_exists($newConfigPath)) { @rename($oldConfigPath, $newConfigPath); } $oldChatPath = getUserChatPath($oldUsername); $newChatPath = getUserChatPath($newUsername); if (file_exists($oldChatPath) && !file_exists($newChatPath)) { @rename($oldChatPath, $newChatPath); } } function hashPassword($password) { return password_hash($password, PASSWORD_DEFAULT); } function verifyPassword($password, $hash) { return password_verify($password, $hash); } function getUserConfig($username) { $username = trim((string) $username); if ($username === '') { return []; } return readUserConfig($username); } function saveUserConfigData($username, $config) { return writeUserConfig($username, $config); } function loadUsers() { ensureConfigDirectory(); $files = glob(USER_CONFIG_DIR . '/*-config.json'); if ($files === false) { $files = []; } $users = []; // load json config files foreach ($files as $file) { $username = basename($file, '-config.json'); $user = readUserConfig($username); if (!empty($user) && !empty($user['username'])) { $users[] = $user; } } // detect legacy php config files and migrate $legacyFiles = glob(USER_CONFIG_DIR . '/*-config.php'); if ($legacyFiles !== false) { foreach ($legacyFiles as $legacy) { $username = basename($legacy, '-config.php'); // if already loaded from json, skip $exists = false; foreach ($users as $u) { if (($u['username'] ?? '') === $username) { $exists = true; break; } } if ($exists) continue; $data = @include $legacy; if (is_array($data) && !empty($data['username'])) { // migrate to json writeUserConfig($username, $data); // remove legacy files @unlink($legacy); $users[] = $data; } } } usort($users, function ($a, $b) { return strcmp(($a['username'] ?? ''), ($b['username'] ?? '')); }); return $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'] = trim((string) ($entry['defaultModel'] ?? '')) !== '' ? trim((string) $entry['defaultModel']) : getConfiguredDefaultModel(); $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.json'); if ($files !== false) { foreach ($files as $file) { $name = basename($file, '-config.json'); if (!in_array($name, $currentUsernames, true)) { @unlink(USER_CONFIG_DIR . '/' . $name . '-config.json'); @unlink(USER_CONFIG_DIR . '/' . $name . '-chat.json'); @unlink(USER_CONFIG_DIR . '/' . $name . '-config.php'); @unlink(USER_CONFIG_DIR . '/' . $name . '-chat.php'); } } } return true; } function findUserByUsername($username) { $username = trim((string) $username); if ($username === '') { return null; } foreach (loadUsers() as $user) { if (($user['username'] ?? '') === $username) { return $user; } } return null; } function findUserByEmail($email) { $email = trim((string) $email); if ($email === '') { return null; } foreach (loadUsers() as $user) { if (strtolower((string) ($user['email'] ?? '')) === strtolower($email)) { return $user; } } return null; } function generateRegistrationCode() { return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); } function sendRegistrationVerificationEmail($email, $code) { $email = trim((string) $email); if ($email === '') { return false; } $subject = 'Your KaI registration verification code'; $message = "Hello,\n\n" . "You requested to create a KaI account.\n" . "Your verification code is: {$code}\n\n" . "Please enter this code on the registration page to complete your account setup."; $headers = "From: " . MAIL_FROM_NAME . " <" . MAIL_FROM . ">\r\n" . "Reply-To: " . MAIL_FROM . "\r\n" . "X-Mailer: PHP/" . phpversion(); return mail($email, $subject, $message, $headers); } function savePendingRegistration($username, $email, $password, $code) { if (session_status() === PHP_SESSION_NONE) { session_start(); } $_SESSION['pending_registration'] = [ 'username' => trim((string) $username), 'email' => trim((string) $email), 'password' => (string) $password, 'code' => (string) $code, 'createdAt' => time(), ]; return true; } function getPendingRegistration() { if (session_status() === PHP_SESSION_NONE) { session_start(); } return $_SESSION['pending_registration'] ?? null; } function clearPendingRegistration() { if (session_status() === PHP_SESSION_NONE) { session_start(); } unset($_SESSION['pending_registration']); } function createUser($username, $password, $role = 'user', $email = '') { $username = trim((string) $username); $email = trim((string) $email); if ($username === '' || $password === '') { return false; } $users = loadUsers(); if (findUserByUsername($username) !== null) { return false; } if ($email !== '' && findUserByEmail($email) !== null) { return false; } $users[] = [ 'username' => $username, 'email' => $email, 'passwordHash' => hashPassword($password), 'role' => $role, 'defaultModel' => getConfiguredDefaultModel() ]; if (!writeUserChat($username, [])) { return false; } return saveUsers($users); } function requestPasswordReset($email) { $email = trim((string) $email); if ($email === '') { return false; } $user = findUserByEmail($email); if ($user === null) { return false; } $token = bin2hex(random_bytes(32)); $users = loadUsers(); foreach ($users as &$entry) { if (($entry['username'] ?? '') === ($user['username'] ?? '')) { $entry['resetTokenHash'] = password_hash($token, PASSWORD_DEFAULT); $entry['resetTokenExpires'] = time() + 3600; break; } } if (!saveUsers($users)) { return false; } $link = APP_BASE_URL . '/reset.php?token=' . urlencode($token); $subject = 'Password reset for KaI'; $message = "Hello {$user['username']},\n\n" . "You requested a password reset for your KaI account.\n" . "Please use the following link to set a new password:\n\n" . $link . "\n\n" . "If you did not request this, you can ignore this email."; $headers = "From: " . MAIL_FROM_NAME . " <" . MAIL_FROM . ">\r\n" . "Reply-To: " . MAIL_FROM . "\r\n" . "X-Mailer: PHP/" . phpversion(); return mail($email, $subject, $message, $headers); } function validatePasswordResetToken($token) { $token = (string) $token; if ($token === '') { return null; } foreach (loadUsers() as $user) { $expiresAt = (int) ($user['resetTokenExpires'] ?? 0); if ($expiresAt > time() && !empty($user['resetTokenHash']) && password_verify($token, (string) $user['resetTokenHash'])) { return $user; } } return null; } function completePasswordReset($token, $newPassword) { $token = (string) $token; $newPassword = (string) $newPassword; if ($token === '' || $newPassword === '') { return false; } $users = loadUsers(); foreach ($users as &$entry) { $expiresAt = (int) ($entry['resetTokenExpires'] ?? 0); if ($expiresAt > time() && !empty($entry['resetTokenHash']) && password_verify($token, (string) $entry['resetTokenHash'])) { $entry['passwordHash'] = hashPassword($newPassword); unset($entry['resetTokenHash'], $entry['resetTokenExpires']); return saveUsers($users); } } return false; } function isLoggedIn() { return !empty($_SESSION['auth_user']['username']); } function getAuthenticatedUser() { if (!isLoggedIn()) { return null; } if (isGuestSession()) { return createGuestUser(); } 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() { if (!isLoggedIn()) { http_response_code(403); echo json_encode(['error' => 'Authentication required.']); exit; } } function requireAdmin() { $user = getAuthenticatedUser(); if (!$user || (($user['role'] ?? 'user') !== 'admin')) { http_response_code(403); echo json_encode(['error' => 'Admin access required.']); exit; } } function logoutUser() { $username = trim((string) ($_SESSION['auth_user']['username'] ?? '')); unset($_SESSION['auth_user']); if ($username !== '') { clearRememberMeCookie($username); } }