You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
543 lines
15 KiB
543 lines
15 KiB
<?php
|
|
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
session_start();
|
|
}
|
|
|
|
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 isGuestModeEnabled() {
|
|
return defined('GUEST_USER_ENABLED') && GUEST_USER_ENABLED === true;
|
|
}
|
|
|
|
function getGuestUsername() {
|
|
return defined('GUEST_USERNAME') ? (string) GUEST_USERNAME : 'guest';
|
|
}
|
|
|
|
function isGuestUsername($username) {
|
|
return isGuestModeEnabled() && strtolower(trim((string) $username)) === strtolower(getGuestUsername());
|
|
}
|
|
|
|
function isGuestSession() {
|
|
return isGuestModeEnabled() && !empty($_SESSION['auth_user']['username']) && isGuestUsername($_SESSION['auth_user']['username']);
|
|
}
|
|
|
|
function createGuestUser() {
|
|
return [
|
|
'username' => 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.php';
|
|
}
|
|
|
|
function getUserChatPath($username) {
|
|
return USER_CONFIG_DIR . '/' . sanitizeUsername($username) . '-chat.php';
|
|
}
|
|
|
|
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)) {
|
|
return [];
|
|
}
|
|
|
|
$data = include $path;
|
|
return is_array($data) ? $data : [];
|
|
}
|
|
|
|
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();
|
|
|
|
$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 [];
|
|
}
|
|
|
|
$data = include $path;
|
|
return is_array($data) ? $data : [];
|
|
}
|
|
|
|
function writeUserChat($username, $chats) {
|
|
ensureConfigDirectory();
|
|
|
|
$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 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.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, $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.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) {
|
|
$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,
|
|
'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);
|
|
}
|
|
}
|