diff --git a/auth.php b/auth.php new file mode 100644 index 0000000..30978c9 --- /dev/null +++ b/auth.php @@ -0,0 +1,247 @@ + '', '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']); +} diff --git a/chat.php b/chat.php index 1eda31d..663f9e9 100644 --- a/chat.php +++ b/chat.php @@ -1,8 +1,7 @@ '']; - } - - $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'] ?? ''; diff --git a/default.css b/default.css index 07eee87..21977b5 100644 --- a/default.css +++ b/default.css @@ -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; } diff --git a/default.php b/default.php index dc44849..3584794 100644 --- a/default.php +++ b/default.php @@ -1,8 +1,10 @@ + + + + + + Forgot Password - KaI + + + +
+

Reset password

+

Enter your email address and we will send you a reset link.

+
+ + +
+
', $errors)); ?>
+
+

Back to login

+
+ + diff --git a/index.html b/index.html index 01c7fa3..d5858b9 100644 --- a/index.html +++ b/index.html @@ -2,378 +2,11 @@ - - - KaI (Ollama Remote Chat) - + + Redirecting to KaI - - -
- -
-
-

I'm KaI your - Ollama Chat

-
- - -
-
-
-
Conversation started.
-
-
- - -
-
-
- - - +

Redirecting to the protected KaI interface...

diff --git a/index.php b/index.php new file mode 100644 index 0000000..8727831 --- /dev/null +++ b/index.php @@ -0,0 +1,282 @@ + + + + + + + + KaI (Ollama Remote Chat) + + + +
+ +
+
+

I'm KaI your - Ollama Chat

+
+ Signed in as + + User Admin + + Logout + + +
+
+
+
Conversation started.
+
+
+ + +
+
+
+ + + + diff --git a/login.php b/login.php new file mode 100644 index 0000000..a39a3ae --- /dev/null +++ b/login.php @@ -0,0 +1,92 @@ + $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'; + } +} +?> + + + + + + Login - KaI + + + +
+

KaI Login

+
+ + + + + + + +
+ +
+ ', $errors)); ?> +
+ + +
+ +
Default admin account is created on first run: admin / changeme
+
Forgot password?
+
+ + diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..2f4ffd9 --- /dev/null +++ b/logout.php @@ -0,0 +1,11 @@ + + + + + + + Reset Password - KaI + + + +
+

Create a new password

+
+ + + +
+
', $errors)); ?>
+
+

Back to login

+
+ + diff --git a/savechat.php b/savechat.php index ab4fd10..d1b8b56 100644 --- a/savechat.php +++ b/savechat.php @@ -1,7 +1,6 @@ = 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(); +?> + + + + + + User Management - KaI + + + +
+
+

User Management

+ Back to chat +
+ +
+
', $errors)); ?>
+ +
+

Create user

+
+ +
+

+

+

+

+
+
+
+
+ +
+

Existing users

+ + + + + + $user): ?> + + + + + + + + +
UsernameEmailRoleActions
+
+ + + +
+ + + + + + + +
+ + + +
+
+
+
+ +