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.

248 lines
6.1 KiB

<?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']);
}