'', 'users' => []]; } $content = @file_get_contents(CONFIG_FILE); if ($content === false) { return ['defaultModel' => '', 'users' => []]; } $data = json_decode($content, true); if (!is_array($data)) { return ['defaultModel' => '', 'users' => []]; } if (!isset($data['users']) || !is_array($data['users'])) { $data['users'] = []; } return $data; } function saveConfigData($config) { ensureConfigDirectory(); $data = getConfigData(); if (array_key_exists('defaultModel', $config)) { $data['defaultModel'] = (string) $config['defaultModel']; } if (array_key_exists('users', $config)) { $data['users'] = is_array($config['users']) ? $config['users'] : []; } $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); return file_put_contents(CONFIG_FILE, $json) !== false; } function readConfig() { $data = getConfigData(); return [ 'defaultModel' => isset($data['defaultModel']) ? (string) $data['defaultModel'] : '', 'users' => isset($data['users']) && is_array($data['users']) ? $data['users'] : [] ]; } function writeConfig($config) { return saveConfigData($config); } function hashPassword($password) { return password_hash($password, PASSWORD_DEFAULT); } function verifyPassword($password, $hash) { return password_verify($password, $hash); } function loadUsers() { $config = readConfig(); return $config['users']; } function saveUsers($users) { return writeConfig(['users' => $users]); } 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 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 ]; 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; } return $_SESSION['auth_user']; } 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() { unset($_SESSION['auth_user']); }