basic user management

main
Steffen Pohle 3 weeks ago
parent 03bf8450e2
commit e59c5cae4b

@ -0,0 +1,247 @@
<?php
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.');
}
}
}
function getConfigData() {
ensureConfigDirectory();
if (!file_exists(CONFIG_FILE)) {
return ['defaultModel' => '', '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']);
}

@ -1,8 +1,7 @@
<?php
// CHANGE THIS to your remote Ollama server IP or domain (e.g., http://192.168.1.50:11434)
define('OLLAMA_URL', 'http://10.2.0.3:11434');
header('Content-Type: application/json');
require_once __DIR__ . '/default.php';
// 1. Handle fetching available models
if (isset($_GET['action']) && $_GET['action'] === 'models') {

@ -2,33 +2,7 @@
header('Content-Type: application/json');
require_once __DIR__ . '/default.php';
function readConfig() {
if (!file_exists(CONFIG_FILE)) {
return ['defaultModel' => ''];
}
$content = file_get_contents(CONFIG_FILE);
if ($content === false) {
return ['defaultModel' => ''];
}
$data = json_decode($content, true);
if (!is_array($data)) {
return ['defaultModel' => ''];
}
return [
'defaultModel' => isset($data['defaultModel']) ? (string) $data['defaultModel'] : ''
];
}
function writeConfig($config) {
$data = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
return file_put_contents(CONFIG_FILE, $data) !== false;
}
require_once __DIR__ . '/auth.php';
$action = $_GET['action'] ?? '';

@ -10,8 +10,13 @@ body {
color:#cdd6f4;
min-height:100vh;
overflow:hidden;
font-family: Arial, sans-serif;
margin: 0;
padding: 24px;
}
.layout {
display:flex;
height:100vh;
@ -100,13 +105,6 @@ button {
cursor:pointer;
}
button.new-chat-btn {
background:#f38ba8;
color:#11111b;
font-weight:bold;
border:none;
}
button.send-btn {
background:#89b4fa;
color:#11111b;
@ -281,7 +279,7 @@ textarea:focus {
}
button.send-btn,
button.new-chat-btn,
button.danger,
select {
width:100%;
}
@ -298,4 +296,15 @@ tt,
font-family: Consolas, "Andale Mono", "Lucida Console", Monaco, "Courier New", monospace;
}
.wrap { max-width: 960px; margin: 0 auto; }
.card { background: #11111b; padding: 20px; border-radius: 10px; margin-bottom: 20px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px; border-bottom: 1px solid #313244; text-align: left; }
input, select, button { padding: 8px; border-radius: 6px; border: 1px solid #45475a; background: #313244; color: #fff; }
button { cursor: pointer; background: #89b4fa; color: #11111b; font-weight: bold; border: none; }
.danger { background: #f38ba8; }
.success { color: #a6e3a1; }
.error { color: #f38ba8; }
.topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
a { color: #89b4fa; }

@ -1,8 +1,10 @@
<?php
header('Content-Type: application/json');
define('CONFIG_PATH', '/home/www/data/kai-ollama');
define('OLLAMA_URL', 'http://127.0.0.1:11434');
define('CONFIG_PATH', '/var/lib/KaI');
define('CONFIG_FILE', CONFIG_PATH . '/config.json');
define('CHAT_HISTORY_FILE', CONFIG_PATH . '/chat.history.data');
define('MAIL_FROM', getenv('MAIL_FROM') ?: 'noreply@kai.local');
define('MAIL_FROM_NAME', getenv('MAIL_FROM_NAME') ?: 'KaI');
define('APP_BASE_URL', getenv('APP_BASE_URL') ?: 'http://127.0.0.1/KaI-OllamaChat');

@ -0,0 +1,53 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/default.php';
require_once __DIR__ . '/auth.php';
$message = '';
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim((string) ($_POST['email'] ?? ''));
if ($email === '') {
$errors[] = 'Please enter your email address.';
} elseif (requestPasswordReset($email)) {
$message = 'If the email exists in our records, a reset link has been sent.';
} else {
$message = 'If the email exists in our records, a reset link has been sent.';
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Forgot Password - KaI</title>
<style>
body { font-family: Arial, sans-serif; background: #1e1e2e; color: #cdd6f4; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
.card { background: #11111b; padding: 24px; border-radius: 10px; width: 360px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
input, button { width: 100%; padding: 10px; border-radius: 6px; border: 1px solid #45475a; background: #313244; color: #fff; margin-top: 10px; }
button { background: #89b4fa; color: #11111b; font-weight: bold; cursor: pointer; }
.message { color: #a6e3a1; margin-top: 12px; }
.error { color: #f38ba8; margin-top: 12px; }
a { color: #89b4fa; }
</style>
</head>
<body>
<div class="card">
<h1>Reset password</h1>
<p>Enter your email address and we will send you a reset link.</p>
<form method="post">
<input type="email" name="email" required placeholder="you@example.com">
<button type="submit">Send reset link</button>
</form>
<?php if (!empty($errors)): ?><div class="error"><?php echo htmlspecialchars(implode('<br>', $errors)); ?></div><?php endif; ?>
<?php if ($message !== ''): ?><div class="message"><?php echo htmlspecialchars($message); ?></div><?php endif; ?>
<p><a href="login.php">Back to login</a></p>
</div>
</body>
</html>

@ -2,378 +2,11 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="default.css">
<title>KaI (Ollama Remote Chat)</title>
<script src="marked.umd.js"></script>
<meta http-equiv="refresh" content="0; url=index.php">
<title>Redirecting to KaI</title>
</head>
<body>
<div class="layout">
<div class="sidebar">
<div class="sidebar-header">
Chat History
</div>
<div id="history-list">
</div>
</div>
<div class="chat-container">
<div class="chat-header">
<h2>I'm KaI your - Ollama Chat</h2>
<div class="controls">
<select id="model-select">
<option>Loading models...</option>
</select>
<button
class="new-chat-btn"
id="new-chat-btn">
New Chat
</button>
</div>
</div>
<div class="chat-messages" id="chat-messages">
<div class="message system">Conversation started.</div>
</div>
<div class="chat-input-area">
<textarea id="user-input" placeholder="Type your message..."></textarea>
<button class="send-btn" id="send-btn">Send</button>
</div>
</div>
</div>
<script>
const chatMessages = document.getElementById('chat-messages');
const historyList = document.getElementById('history-list');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const newChatBtn = document.getElementById('new-chat-btn');
const modelSelect = document.getElementById('model-select');
let chatHistory = [];
let currentChatId = null;
marked.setOptions({
breaks:true
});
/* ==========================================================
Message Output
========================================================== */
function appendMessage(role, text) {
const msgDiv = document.createElement("div");
msgDiv.classList.add("message", role);
if (role === "assistant") {
msgDiv.innerHTML = marked.parse(text);
} else {
msgDiv.textContent = text;
}
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
return msgDiv;
}
/* ==========================================================
Models
========================================================== */
async function loadConfig() {
try {
const response = await fetch("config.php?action=load");
const data = await response.json();
return data.defaultModel || "";
} catch (err) {
console.error(err);
return "";
}
}
async function saveConfig(defaultModel) {
try {
await fetch("config.php?action=save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ defaultModel })
});
} catch (err) {
console.error(err);
}
}
async function loadModels() {
try {
const response = await fetch("chat.php?action=models");
const data = await response.json();
const defaultModel = await loadConfig();
modelSelect.innerHTML = "";
if (data.models) {
let selectedModel = "";
data.models.forEach(model => {
const option = document.createElement("option");
option.value = model.name;
option.textContent = model.name;
modelSelect.appendChild(option);
if (!selectedModel && model.name === defaultModel) {
selectedModel = model.name;
}
});
const fallbackModel = selectedModel || (data.models[0] && data.models[0].name) || "";
if (fallbackModel) {
modelSelect.value = fallbackModel;
} else {
modelSelect.innerHTML = "<option>No Models</option>";
}
} else {
modelSelect.innerHTML = "<option>No Models</option>";
}
}
catch {
modelSelect.innerHTML = "<option>Connection failed</option>";
}
}
/* ==========================================================
History
========================================================== */
async function loadHistory() {
try {
const response = await fetch("savechat.php?action=list");
const chats = await response.json();
historyList.innerHTML = "";
chats.forEach(chat => {
const item = document.createElement("div");
item.className = "history-item";
if (chat.id === currentChatId) item.classList.add("active");
item.innerHTML = `<div class="history-title">${chat.title}</div>`;
const historyDateDiv = document.createElement("div");
const historyDate = chat.created || '';
historyDateDiv.textContent = historyDate;
historyDateDiv.className = "history-date";
item.appendChild(historyDateDiv);
const historyModelSpan = document.createElement("span");
historyModelSpan.textContent = `${chat.model}`;
historyModelSpan.style.display = "inline-block";
historyModelSpan.style.marginLeft = "10px";
historyModelSpan.style.float = "right";
historyModelSpan.className = "history-date";
item.appendChild(historyModelSpan);
item.appendChild(historyDateDiv);
item.onclick = () => {
loadChat(chat.id);
};
// add a delete button here
const deleteBtn = document.createElement("button");
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = async () => {
// remove the chat from the list and from the database
historyList.removeChild(item);
await fetch('savechat.php?action=delete&id=' + encodeURIComponent(chat.id));
loadHistory(); // reload the history to update the list
};
item.appendChild(deleteBtn);
historyList.appendChild(item);
});
} catch (err) {
console.error(err);
}
}
/* ==========================================================
Load Chat
========================================================== */
async function loadChat(id) {
try {
const response =
await fetch(
"savechat.php?action=load&id="
+ encodeURIComponent(id)
);
const chat = await response.json();
if (chat.error) {
alert(chat.error);
return;
}
currentChatId = chat.id;
chatHistory = chat.messages;
chatMessages.innerHTML = "";
chat.messages.forEach(msg => {
appendMessage(
msg.role,
msg.content
);
});
loadHistory();
}
catch(err) {
console.error(err);
}
}
/* ==========================================================
Save Chat
========================================================== */
async function saveChat() {
if (chatHistory.length === 0) return;
try {
const response = await fetch( "savechat.php?action=save", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({id: currentChatId, model: modelSelect.value, messages: chatHistory})});
const result = await response.json();
if (result.id) currentChatId = result.id;
loadHistory();
}
catch(err) {
console.error(err);
}
}
/* ==========================================================
Send Message
========================================================== */
async function sendMessage() {
const text = userInput.value.trim();
if (!text) return;
if (!modelSelect.value) {
alert("Please select a model.");
return;
}
appendMessage("user", text);
chatHistory.push({role: "user", content: text});
userInput.value = "";
const thinking =
appendMessage( "assistant",'<div class="thinking-text">Thinking<span>.</span><span>.</span><span>.</span></div>');
thinking.classList.add("thinking-state");
const startTime = performance.now();
try {
const response =
await fetch("chat.php", {
method: "POST",
headers: { "Content-Type":"application/json" },
body: JSON.stringify({ model: modelSelect.value, messages: chatHistory})
});
thinking.classList.remove("thinking-state");
const data = await response.json();
const endTime = performance.now();
const totalDuration = ((endTime-startTime)/1000).toFixed(2);
if (data.error) {
thinking.textContent = data.error;
return;
}
const html = marked.parse(data.message.content);
const tokens = data.eval_count || 0;
const promptTokens = data.prompt_eval_count || 0;
const generationSeconds = data.eval_duration ? data.eval_duration / 1000000000 : 0;
const tps = (tokens>0 && generationSeconds>0) ? (tokens/generationSeconds).toFixed(1) : 0;
let footer = "Time: " + totalDuration + " sec";
if(tokens>0){
footer += " | Prompt " + promptTokens + " t | Answer " + tokens
+ " t (" + tps + " t/s)";
}
thinking.innerHTML =
html +
'<div style="margin-top:8px;font-size:11px;color:#999;text-align:right;font-style:italic;">'
+ footer +
'</div>';
chatHistory.push({
role:"assistant",
content:data.message.content
});
await saveChat();
}
catch(err){
console.error(err);
thinking.classList.remove("thinking-state");
thinking.textContent =
"Backend connection failed.";
}
}
/* ==========================================================
New Chat
========================================================== */
newChatBtn.addEventListener("click", () => {
currentChatId = null;
chatHistory = [];
chatMessages.innerHTML = "";
appendMessage("system","Conversation started.");
loadHistory();
});
modelSelect.addEventListener("change", () => {
if (modelSelect.value && modelSelect.value !== "Loading models..." && modelSelect.value !== "No Models" && modelSelect.value !== "Connection failed") {
saveConfig(modelSelect.value);
}
});
/* ==========================================================
Keyboard
========================================================== */
sendBtn.addEventListener("click",sendMessage);
userInput.addEventListener("keydown", function(e){
if(e.key==="Enter" && !e.shiftKey){
e.preventDefault();
sendMessage();
}
});
/* ==========================================================
Startup
========================================================== */
loadModels();
loadHistory();
</script>
<p>Redirecting to the protected KaI interface...</p>
</body>
</html>

@ -0,0 +1,282 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/default.php';
require_once __DIR__ . '/auth.php';
if (!isLoggedIn()) {
header('Location: login.php');
exit;
}
$authUser = getAuthenticatedUser();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="default.css">
<title>KaI (Ollama Remote Chat)</title>
<script src="marked.umd.js"></script>
</head>
<body>
<div class="layout">
<div class="sidebar">
<div class="sidebar-header">
Chat History
</div>
<div id="history-list"></div>
</div>
<div class="chat-container">
<div class="chat-header">
<h2>I'm KaI your - Ollama Chat</h2>
<div class="controls">
<span style="font-size:12px; color:#aaa;">Signed in as <?php echo htmlspecialchars($authUser['username'] ?? ''); ?></span>
<?php if (($authUser['role'] ?? 'user') === 'admin'): ?>
<a href="useradmin.php" style="color:#89b4fa; text-decoration:none;">User Admin</a>
<?php endif; ?>
<a href="logout.php" style="color:#f38ba8; text-decoration:none;">Logout</a>
<select id="model-select">
<option>Loading models...</option>
</select>
<button class="danger" id="new-chat-btn">New Chat</button>
</div>
</div>
<div class="chat-messages" id="chat-messages">
<div class="message system">Conversation started.</div>
</div>
<div class="chat-input-area">
<textarea id="user-input" placeholder="Type your message..."></textarea>
<button class="send-btn" id="send-btn">Send</button>
</div>
</div>
</div>
<script>
const chatMessages = document.getElementById('chat-messages');
const historyList = document.getElementById('history-list');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const newChatBtn = document.getElementById('new-chat-btn');
const modelSelect = document.getElementById('model-select');
let chatHistory = [];
let currentChatId = null;
marked.setOptions({ breaks:true });
function appendMessage(role, text) {
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', role);
if (role === 'assistant') {
msgDiv.innerHTML = marked.parse(text);
} else {
msgDiv.textContent = text;
}
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
return msgDiv;
}
async function loadConfig() {
try {
const response = await fetch('config.php?action=load');
const data = await response.json();
return data.defaultModel || '';
} catch (err) {
console.error(err);
return '';
}
}
async function saveConfig(defaultModel) {
try {
await fetch('config.php?action=save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ defaultModel })
});
} catch (err) {
console.error(err);
}
}
async function loadModels() {
try {
const response = await fetch('chat.php?action=models');
const data = await response.json();
const defaultModel = await loadConfig();
modelSelect.innerHTML = '';
if (data.models) {
let selectedModel = '';
data.models.forEach(model => {
const option = document.createElement('option');
option.value = model.name;
option.textContent = model.name;
modelSelect.appendChild(option);
if (!selectedModel && model.name === defaultModel) {
selectedModel = model.name;
}
});
const fallbackModel = selectedModel || (data.models[0] && data.models[0].name) || '';
if (fallbackModel) {
modelSelect.value = fallbackModel;
} else {
modelSelect.innerHTML = '<option>No Models</option>';
}
} else {
modelSelect.innerHTML = '<option>No Models</option>';
}
} catch {
modelSelect.innerHTML = '<option>Connection failed</option>';
}
}
async function loadHistory() {
try {
const response = await fetch('savechat.php?action=list');
const chats = await response.json();
historyList.innerHTML = '';
chats.forEach(chat => {
const item = document.createElement('div');
item.className = 'history-item';
if (chat.id === currentChatId) item.classList.add('active');
item.innerHTML = `<div class="history-title">${chat.title}</div>`;
const historyDateDiv = document.createElement('div');
historyDateDiv.textContent = chat.created || '';
historyDateDiv.className = 'history-date';
item.appendChild(historyDateDiv);
const historyModelSpan = document.createElement('span');
historyModelSpan.textContent = `${chat.model}`;
historyModelSpan.style.display = 'inline-block';
historyModelSpan.style.marginLeft = '10px';
historyModelSpan.style.float = 'right';
historyModelSpan.className = 'history-date';
item.appendChild(historyModelSpan);
item.onclick = () => loadChat(chat.id);
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = async (event) => {
event.stopPropagation();
historyList.removeChild(item);
await fetch('savechat.php?action=delete&id=' + encodeURIComponent(chat.id));
loadHistory();
};
item.appendChild(deleteBtn);
historyList.appendChild(item);
});
} catch (err) {
console.error(err);
}
}
async function loadChat(id) {
try {
const response = await fetch('savechat.php?action=load&id=' + encodeURIComponent(id));
const chat = await response.json();
if (chat.error) {
alert(chat.error);
return;
}
currentChatId = chat.id;
chatHistory = chat.messages;
chatMessages.innerHTML = '';
chat.messages.forEach(msg => appendMessage(msg.role, msg.content));
loadHistory();
} catch (err) {
console.error(err);
}
}
async function saveChat() {
if (chatHistory.length === 0) return;
try {
const response = await fetch('savechat.php?action=save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: currentChatId, model: modelSelect.value, messages: chatHistory })
});
const result = await response.json();
if (result.id) currentChatId = result.id;
loadHistory();
} catch (err) {
console.error(err);
}
}
async function sendMessage() {
const text = userInput.value.trim();
if (!text) return;
if (!modelSelect.value) {
alert('Please select a model.');
return;
}
appendMessage('user', text);
chatHistory.push({ role: 'user', content: text });
userInput.value = '';
const thinking = appendMessage('assistant', '<div class="thinking-text">Thinking<span>.</span><span>.</span><span>.</span></div>');
thinking.classList.add('thinking-state');
const startTime = performance.now();
try {
const response = await fetch('chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: modelSelect.value, messages: chatHistory })
});
thinking.classList.remove('thinking-state');
const data = await response.json();
const endTime = performance.now();
const totalDuration = ((endTime - startTime) / 1000).toFixed(2);
if (data.error) {
thinking.textContent = data.error;
return;
}
const html = marked.parse(data.message.content);
const tokens = data.eval_count || 0;
const promptTokens = data.prompt_eval_count || 0;
const generationSeconds = data.eval_duration ? data.eval_duration / 1000000000 : 0;
const tps = (tokens > 0 && generationSeconds > 0) ? (tokens / generationSeconds).toFixed(1) : 0;
let footer = 'Time: ' + totalDuration + ' sec';
if (tokens > 0) {
footer += ' | Prompt ' + promptTokens + ' t | Answer ' + tokens + ' t (' + tps + ' t/s)';
}
thinking.innerHTML = html + '<div style="margin-top:8px;font-size:11px;color:#999;text-align:right;font-style:italic;">' + footer + '</div>';
chatHistory.push({ role: 'assistant', content: data.message.content });
await saveChat();
} catch (err) {
console.error(err);
thinking.classList.remove('thinking-state');
thinking.textContent = 'Backend connection failed.';
}
}
newChatBtn.addEventListener('click', () => {
currentChatId = null;
chatHistory = [];
chatMessages.innerHTML = '';
appendMessage('system', 'Conversation started.');
loadHistory();
});
modelSelect.addEventListener('change', () => {
if (modelSelect.value && modelSelect.value !== 'Loading models...' && modelSelect.value !== 'No Models' && modelSelect.value !== 'Connection failed') {
saveConfig(modelSelect.value);
}
});
sendBtn.addEventListener('click', sendMessage);
userInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
loadModels();
loadHistory();
</script>
</body>
</html>

@ -0,0 +1,92 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/default.php';
require_once __DIR__ . '/auth.php';
$errors = [];
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = (string) ($_POST['password'] ?? '');
if ($username === '' || $password === '') {
$errors[] = 'Please enter both username and password.';
} else {
$user = findUserByUsername($username);
if ($user && verifyPassword($password, $user['passwordHash'] ?? '')) {
$_SESSION['auth_user'] = [
'username' => $user['username'],
'role' => $user['role'] ?? 'user'
];
header('Location: index.php');
exit;
}
$errors[] = 'Invalid username or password.';
}
}
if (isLoggedIn()) {
header('Location: index.php');
exit;
}
if (!file_exists(CONFIG_FILE)) {
ensureConfigDirectory();
}
if (empty(loadUsers())) {
$created = createUser('admin', 'changeme', 'admin', 'admin@example.com');
if ($created) {
$message = 'Default admin account created. Username: admin / Password: changeme';
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - KaI</title>
<style>
body { font-family: Arial, sans-serif; background: #1e1e2e; color: #cdd6f4; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
.card { background: #11111b; padding: 24px; border-radius: 10px; width: 360px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
h1 { margin-top: 0; font-size: 1.4rem; }
label { display: block; margin-top: 12px; font-weight: bold; }
input { width: 100%; padding: 10px; margin-top: 6px; border-radius: 6px; border: 1px solid #45475a; background: #313244; color: #fff; }
button { margin-top: 16px; width: 100%; padding: 10px; border: none; border-radius: 6px; background: #89b4fa; color: #11111b; font-weight: bold; cursor: pointer; }
.error { color: #f38ba8; margin-top: 12px; }
.message { color: #a6e3a1; margin-top: 12px; }
.small { font-size: 0.9rem; color: #999; margin-top: 12px; }
</style>
</head>
<body>
<div class="card">
<h1>KaI Login</h1>
<form method="post">
<label for="username">Username</label>
<input id="username" name="username" required>
<label for="password">Password</label>
<input id="password" name="password" type="password" required>
<button type="submit">Sign in</button>
</form>
<?php if (!empty($errors)): ?>
<div class="error">
<?php echo htmlspecialchars(implode('<br>', $errors)); ?>
</div>
<?php endif; ?>
<?php if ($message !== ''): ?>
<div class="message"><?php echo htmlspecialchars($message); ?></div>
<?php endif; ?>
<div class="small">Default admin account is created on first run: admin / changeme</div>
<div class="small"><a href="forgot.php" style="color:#89b4fa;">Forgot password?</a></div>
</div>
</body>
</html>

@ -0,0 +1,11 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/auth.php';
logoutUser();
header('Location: login.php');
exit;

@ -0,0 +1,57 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/default.php';
require_once __DIR__ . '/auth.php';
$message = '';
$errors = [];
$token = (string) ($_GET['token'] ?? '');
if ($token === '') {
$errors[] = 'Missing reset token.';
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = (string) ($_POST['token'] ?? '');
$newPassword = (string) ($_POST['password'] ?? '');
if ($token === '' || $newPassword === '') {
$errors[] = 'Please provide a new password.';
} elseif (completePasswordReset($token, $newPassword)) {
$message = 'Password updated successfully. You can now log in.';
} else {
$errors[] = 'The reset link is invalid or expired.';
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reset Password - KaI</title>
<style>
body { font-family: Arial, sans-serif; background: #1e1e2e; color: #cdd6f4; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
.card { background: #11111b; padding: 24px; border-radius: 10px; width: 360px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
input, button { width: 100%; padding: 10px; border-radius: 6px; border: 1px solid #45475a; background: #313244; color: #fff; margin-top: 10px; }
button { background: #89b4fa; color: #11111b; font-weight: bold; cursor: pointer; }
.message { color: #a6e3a1; margin-top: 12px; }
.error { color: #f38ba8; margin-top: 12px; }
a { color: #89b4fa; }
</style>
</head>
<body>
<div class="card">
<h1>Create a new password</h1>
<form method="post">
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token); ?>">
<input type="password" name="password" required placeholder="New password">
<button type="submit">Save new password</button>
</form>
<?php if (!empty($errors)): ?><div class="error"><?php echo htmlspecialchars(implode('<br>', $errors)); ?></div><?php endif; ?>
<?php if ($message !== ''): ?><div class="message"><?php echo htmlspecialchars($message); ?></div><?php endif; ?>
<p><a href="login.php">Back to login</a></p>
</div>
</body>
</html>

@ -1,7 +1,6 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/default.php';
/*----------------------------------------------------------

@ -0,0 +1,151 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/default.php';
require_once __DIR__ . '/auth.php';
requireAuthentication();
requireAdmin();
$users = loadUsers();
$errors = [];
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'create') {
$username = trim((string) ($_POST['username'] ?? ''));
$password = (string) ($_POST['password'] ?? '');
$email = trim((string) ($_POST['email'] ?? ''));
$role = ($_POST['role'] ?? 'user') === 'admin' ? 'admin' : 'user';
if ($username === '' || $password === '') {
$errors[] = 'Username and password are required.';
} elseif (!createUser($username, $password, $role, $email)) {
$errors[] = 'User already exists or could not be created.';
} else {
$users = loadUsers();
$success = 'User created successfully.';
}
} elseif ($action === 'update') {
$index = (int) ($_POST['index'] ?? -1);
$username = trim((string) ($_POST['username'] ?? ''));
$email = trim((string) ($_POST['email'] ?? ''));
$password = (string) ($_POST['password'] ?? '');
$role = ($_POST['role'] ?? 'user') === 'admin' ? 'admin' : 'user';
if ($index >= 0 && isset($users[$index])) {
if ($username !== '') {
$users[$index]['username'] = $username;
}
if ($email !== '') {
$users[$index]['email'] = $email;
}
if ($password !== '') {
$users[$index]['passwordHash'] = hashPassword($password);
}
$users[$index]['role'] = $role;
if (saveUsers($users)) {
$success = 'User updated successfully.';
} else {
$errors[] = 'Unable to update user.';
}
} else {
$errors[] = 'Invalid user selected.';
}
} elseif ($action === 'delete') {
$index = (int) ($_POST['index'] ?? -1);
if ($index >= 0 && isset($users[$index])) {
unset($users[$index]);
$users = array_values($users);
if (saveUsers($users)) {
$success = 'User deleted successfully.';
} else {
$errors[] = 'Unable to delete user.';
}
} else {
$errors[] = 'Invalid user selected.';
}
}
}
$users = loadUsers();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Management - KaI</title>
<link rel="stylesheet" type="text/css" href="default.css">
</head>
<body>
<div class="wrap">
<div class="topbar">
<h1>User Management</h1>
<a href="index.php">Back to chat</a>
</div>
<?php if ($success !== ''): ?><div class="success"><?php echo htmlspecialchars($success); ?></div><?php endif; ?>
<?php if (!empty($errors)): ?><div class="error"><?php echo htmlspecialchars(implode('<br>', $errors)); ?></div><?php endif; ?>
<div class="card">
<h2>Create user</h2>
<form method="post">
<input type="hidden" name="action" value="create">
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:10px;">
<div><label>Username</label><br><input name="username" required></div>
<div><label>Email</label><br><input name="email" type="email"></div>
<div><label>Password</label><br><input name="password" type="password" required></div>
<div><label>Role</label><br><select name="role"><option value="user">User</option><option value="admin">Admin</option></select></div>
<div><label>&nbsp;</label><button type="submit">Create</button></div>
</div>
</form>
</div>
<div class="card">
<h2>Existing users</h2>
<table>
<thead>
<tr><th>Username</th><th>Email</th><th>Role</th><th>Actions</th></tr>
</thead>
<tbody>
<?php foreach ($users as $index => $user): ?>
<tr>
<td>
<form method="post" style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
<input type="hidden" name="action" value="update">
<input type="hidden" name="index" value="<?php echo (int) $index; ?>">
<input name="username" value="<?php echo htmlspecialchars($user['username'] ?? ''); ?>" style="width: 150px"; required>
</td>
<td>
<input name="email" type="email" value="<?php echo htmlspecialchars($user['email'] ?? ''); ?>">
</td>
<td>
<select name="role">
<option value="user" <?php if (($user['role'] ?? 'user') === 'user') echo 'selected'; ?>>User</option>
<option value="admin" <?php if (($user['role'] ?? 'user') === 'admin') echo 'selected'; ?>>Admin</option>
</select>
</td>
<td>
<input name="password" type="password" placeholder="New password" style="width: 150px">
<button type="submit">Save</button>
</form>
<form method="post" style="display:inline-block; margin-left:8px;">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="index" value="<?php echo (int) $index; ?>">
<button class="danger" type="submit">Delete</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</body>
</html>
Loading…
Cancel
Save