|
|
|
|
@ -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 = "<?php\n\nreturn " . var_export($data, true) . ";\n";
|
|
|
|
|
return file_put_contents(getUserConfigPath($username), $php) !== false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readUserChat($username) {
|
|
|
|
|
$path = getUserChatPath($username);
|
|
|
|
|
if (!file_exists($path)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
|
|
|
return file_put_contents(CONFIG_FILE, $json) !== false;
|
|
|
|
|
$data = include $path;
|
|
|
|
|
return is_array($data) ? $data : [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readConfig() {
|
|
|
|
|
$data = getConfigData();
|
|
|
|
|
function writeUserChat($username, $chats) {
|
|
|
|
|
ensureConfigDirectory();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'defaultModel' => 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 = "<?php\n\nreturn " . var_export(is_array($chats) ? $chats : [], true) . ";\n";
|
|
|
|
|
return file_put_contents(getUserChatPath($username), $php) !== false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function writeConfig($config) {
|
|
|
|
|
return saveConfigData($config);
|
|
|
|
|
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) {
|
|
|
|
|
@ -73,13 +216,92 @@ 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() {
|
|
|
|
|
$config = readConfig();
|
|
|
|
|
return $config['users'];
|
|
|
|
|
ensureConfigDirectory();
|
|
|
|
|
|
|
|
|
|
$files = glob(USER_CONFIG_DIR . '/*-config.php');
|
|
|
|
|
if ($files === false) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$users = [];
|
|
|
|
|
foreach ($files as $file) {
|
|
|
|
|
$username = basename($file, '-config.php');
|
|
|
|
|
$user = readUserConfig($username);
|
|
|
|
|
if (!empty($user) && !empty($user['username'])) {
|
|
|
|
|
$users[] = $user;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
usort($users, function ($a, $b) {
|
|
|
|
|
return strcmp(($a['username'] ?? ''), ($b['username'] ?? ''));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return $users;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function saveUsers($users) {
|
|
|
|
|
return writeConfig(['users' => $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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|