Merge pull request 'Fix' (#61) from dev into main
Reviewed-on: #61
This commit was merged in pull request #61.
This commit is contained in:
@@ -210,24 +210,36 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
// 4. AI Co-Pilot instellingen opslaan (Release v1.5)
|
// 4. AI Co-Pilot instellingen opslaan (Release v1.5)
|
||||||
elseif ($action === 'save_ai_settings') {
|
elseif ($action === 'save_ai_settings') {
|
||||||
$aiEnabled = isset($_POST['ai_enabled']) ? 1 : 0;
|
try {
|
||||||
$aiProvider = trim($_POST['ai_provider'] ?? 'openai');
|
$aiEnabled = isset($_POST['ai_enabled']) ? 1 : 0;
|
||||||
$aiModel = trim($_POST['ai_model'] ?? 'gpt-4o-mini');
|
$aiModel = trim((string)($_POST['ai_model'] ?? 'gemini-1.5-pro'));
|
||||||
$apiKeyIn = trim($_POST['ai_api_key'] ?? '');
|
$aiProvider = trim((string)($_POST['ai_provider'] ?? ''));
|
||||||
|
if (empty($aiProvider)) {
|
||||||
|
$aiProvider = str_contains($aiModel, 'gpt') ? 'openai' : 'gemini';
|
||||||
|
}
|
||||||
|
$apiKeyIn = trim((string)($_POST['ai_api_key'] ?? ''));
|
||||||
|
|
||||||
$currentAi = get_ai_settings();
|
if ($apiKeyIn !== '' && $apiKeyIn !== '••••••••••••••••') {
|
||||||
if ($apiKeyIn !== '' && $apiKeyIn !== '••••••••••••••••') {
|
set_system_setting('ai_api_key', fociz_encrypt($apiKeyIn));
|
||||||
set_system_setting('ai_api_key', fociz_encrypt($apiKeyIn));
|
}
|
||||||
|
|
||||||
|
set_system_setting('ai_enabled', $aiEnabled);
|
||||||
|
set_system_setting('ai_provider', $aiProvider);
|
||||||
|
set_system_setting('ai_model', $aiModel);
|
||||||
|
|
||||||
|
fociz_log_audit($userId, 'SETTINGS_UPDATED', 'ai_settings', null, [
|
||||||
|
'ai_enabled' => $aiEnabled,
|
||||||
|
'ai_provider' => $aiProvider,
|
||||||
|
'ai_model' => $aiModel
|
||||||
|
], 'SUCCESS', $adminUser['email'] ?? null);
|
||||||
|
|
||||||
|
$message = "AI Co-Pilot instellingen zijn succesvol opgeslagen.";
|
||||||
|
$messageType = 'success';
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log("Fout bij opslaan AI instellingen: " . $e->getMessage());
|
||||||
|
$message = "Fout bij opslaan van AI instellingen: " . htmlspecialchars($e->getMessage());
|
||||||
|
$messageType = 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
set_system_setting('ai_enabled', $aiEnabled);
|
|
||||||
set_system_setting('ai_provider', $aiProvider);
|
|
||||||
set_system_setting('ai_model', $aiModel);
|
|
||||||
|
|
||||||
fociz_log_audit($userId, 'SETTINGS_UPDATED', 'ai_settings', null, ['ai_enabled' => $aiEnabled, 'ai_provider' => $aiProvider, 'ai_model' => $aiModel], 'SUCCESS', $adminUser['email']);
|
|
||||||
|
|
||||||
$message = "AI Co-Pilot instellingen zijn succesvol opgeslagen.";
|
|
||||||
$messageType = 'success';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Test e-mail versturen via SMTP (Release v1.6)
|
// 5. Test e-mail versturen via SMTP (Release v1.6)
|
||||||
@@ -1098,7 +1110,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</span>
|
</span>
|
||||||
<span class="px-2.5 py-1 rounded-full <?= $aiSettings['has_key'] ? 'bg-teal-50 dark:bg-teal-950/40 text-teal-700 dark:text-teal-400 border border-teal-200 dark:border-teal-800/60' : 'bg-amber-50 dark:bg-amber-950/40 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-800/60' ?> text-xs font-bold inline-flex items-center space-x-1.5">
|
<span class="px-2.5 py-1 rounded-full <?= $aiSettings['has_key'] ? 'bg-teal-50 dark:bg-teal-950/40 text-teal-700 dark:text-teal-400 border border-teal-200 dark:border-teal-800/60' : 'bg-amber-50 dark:bg-amber-950/40 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-800/60' ?> text-xs font-bold inline-flex items-center space-x-1.5">
|
||||||
<i data-lucide="<?= $aiSettings['has_key'] ? 'check-circle-2' : 'key' ?>" class="w-3.5 h-3.5 flex-shrink-0"></i>
|
<i data-lucide="<?= $aiSettings['has_key'] ? 'check-circle-2' : 'key' ?>" class="w-3.5 h-3.5 flex-shrink-0"></i>
|
||||||
<span><?= $aiSettings['has_key'] ? 'AI Actief (' . htmlspecialchars($aiSettings['ai_model']) . ')' : 'Sleutel Vereist' ?></span>
|
<span><?= $aiSettings['has_key'] ? 'AI Actief (' . htmlspecialchars((string)($aiSettings['ai_model'] ?? 'Gemini Pro')) . ')' : 'Sleutel Vereist' ?></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1141,7 +1153,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
<option value="gpt-4o" <?= $aiSettings['ai_model'] === 'gpt-4o' ? 'selected' : '' ?>>OpenAI GPT-4o</option>
|
<option value="gpt-4o" <?= $aiSettings['ai_model'] === 'gpt-4o' ? 'selected' : '' ?>>OpenAI GPT-4o</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
<input type="hidden" name="ai_provider" id="admin_ai_provider" value="<?= strpos($aiSettings['ai_model'], 'gpt') !== false ? 'openai' : 'gemini' ?>">
|
<input type="hidden" name="ai_provider" id="admin_ai_provider" value="<?= str_contains((string)($aiSettings['ai_model'] ?? ''), 'gpt') ? 'openai' : 'gemini' ?>">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -1651,7 +1663,10 @@ async function testAdminAiConnection() {
|
|||||||
const resultDiv = document.getElementById('adminAiTestResult');
|
const resultDiv = document.getElementById('adminAiTestResult');
|
||||||
const apiKey = document.getElementById('admin_ai_api_key').value.trim();
|
const apiKey = document.getElementById('admin_ai_api_key').value.trim();
|
||||||
const model = document.getElementById('admin_ai_model').value;
|
const model = document.getElementById('admin_ai_model').value;
|
||||||
const provider = document.getElementById('admin_ai_provider').value;
|
let provider = document.getElementById('admin_ai_provider').value;
|
||||||
|
if (!provider) {
|
||||||
|
provider = (model && model.indexOf('gpt') !== -1) ? 'openai' : 'gemini';
|
||||||
|
}
|
||||||
|
|
||||||
resultDiv.classList.add('hidden');
|
resultDiv.classList.add('hidden');
|
||||||
resultDiv.className = 'hidden p-3 rounded-xl text-xs';
|
resultDiv.className = 'hidden p-3 rounded-xl text-xs';
|
||||||
@@ -1673,12 +1688,19 @@ async function testAdminAiConnection() {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
|
||||||
|
const rawText = await res.text();
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(rawText);
|
||||||
|
} catch (parseErr) {
|
||||||
|
throw new Error(`Ongeldig serverantwoord (HTTP ${res.status}): ${rawText.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
|
||||||
resultDiv.classList.remove('hidden');
|
resultDiv.classList.remove('hidden');
|
||||||
if (data.success) {
|
if (data && data.success) {
|
||||||
resultDiv.className = 'p-3 rounded-xl text-xs bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200 dark:border-emerald-800 text-emerald-800 dark:text-emerald-200 flex items-center space-x-2';
|
resultDiv.className = 'p-3 rounded-xl text-xs bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200 dark:border-emerald-800 text-emerald-800 dark:text-emerald-200 flex items-center space-x-2';
|
||||||
resultDiv.innerHTML = `<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0"></i><span><strong>Verbinding Geslaagd!</strong> ${data.message}</span>`;
|
resultDiv.innerHTML = `<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0"></i><span><strong>Verbinding Geslaagd!</strong> ${data.message || 'API verbinding is geverifieerd.'}</span>`;
|
||||||
} else {
|
} else {
|
||||||
resultDiv.className = 'p-3 rounded-xl text-xs bg-rose-50 dark:bg-rose-950/30 border border-rose-200 dark:border-rose-800 text-rose-800 dark:text-rose-200 flex items-center space-x-2';
|
resultDiv.className = 'p-3 rounded-xl text-xs bg-rose-50 dark:bg-rose-950/30 border border-rose-200 dark:border-rose-800 text-rose-800 dark:text-rose-200 flex items-center space-x-2';
|
||||||
resultDiv.innerHTML = `<i data-lucide="alert-circle" class="w-4 h-4 text-rose-600 dark:text-rose-400 flex-shrink-0"></i><span><strong>Verbinding Mislukt:</strong> ${data.error || 'Onbekende fout opgetreden'}</span>`;
|
resultDiv.innerHTML = `<i data-lucide="alert-circle" class="w-4 h-4 text-rose-600 dark:text-rose-400 flex-shrink-0"></i><span><strong>Verbinding Mislukt:</strong> ${data.error || 'Onbekende fout opgetreden'}</span>`;
|
||||||
|
|||||||
+77
-58
@@ -351,71 +351,86 @@ try {
|
|||||||
|
|
||||||
// 3d. Test AI Verbinding (Gemini Pro / OpenAI)
|
// 3d. Test AI Verbinding (Gemini Pro / OpenAI)
|
||||||
if ($action === 'test_connection') {
|
if ($action === 'test_connection') {
|
||||||
$data = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
try {
|
||||||
|
$rawInput = file_get_contents('php://input');
|
||||||
|
$data = !empty($rawInput) ? (json_decode($rawInput, true) ?: []) : $_POST;
|
||||||
|
|
||||||
$tempProvider = !empty($data['provider']) ? trim($data['provider']) : null;
|
$tempProvider = !empty($data['provider']) ? trim((string)$data['provider']) : null;
|
||||||
$tempKey = !empty($data['api_key']) ? trim($data['api_key']) : null;
|
$tempKey = !empty($data['api_key']) ? trim((string)$data['api_key']) : null;
|
||||||
$tempModel = !empty($data['model']) ? trim($data['model']) : null;
|
$tempModel = !empty($data['model']) ? trim((string)$data['model']) : null;
|
||||||
|
|
||||||
// Als er test credentials zijn meegegeven, test direct met die credentials
|
// Als er test credentials zijn meegegeven, test direct met die credentials
|
||||||
if (!empty($tempKey)) {
|
if (!empty($tempKey) && $tempKey !== '••••••••••••••••') {
|
||||||
$testSettings = [
|
$testSettings = [
|
||||||
'ai_provider' => $tempProvider ?: 'gemini',
|
'ai_provider' => $tempProvider ?: 'gemini',
|
||||||
'ai_api_key' => $tempKey,
|
'ai_api_key' => $tempKey,
|
||||||
'ai_model' => $tempModel ?: ($tempProvider === 'openai' ? 'gpt-4o-mini' : 'gemini-1.5-pro'),
|
'ai_model' => $tempModel ?: ($tempProvider === 'openai' ? 'gpt-4o-mini' : 'gemini-1.5-pro'),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Anders haal de geconfigureerde instellingen op van user of org
|
||||||
|
$testSettings = get_ai_settings($user['id']);
|
||||||
|
if (!empty($tempModel)) {
|
||||||
|
$testSettings['ai_model'] = $tempModel;
|
||||||
|
}
|
||||||
|
if (!empty($tempProvider)) {
|
||||||
|
$testSettings['ai_provider'] = $tempProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($testSettings['ai_api_key'])) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Geen API-sleutel ingevuld of geconfigureerd om te testen.',
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$provider = strtolower($testSettings['ai_provider'] ?? 'gemini');
|
||||||
|
$model = $testSettings['ai_model'] ?? ($provider === 'openai' ? 'gpt-4o-mini' : 'gemini-1.5-pro');
|
||||||
|
|
||||||
|
$t0 = microtime(true);
|
||||||
|
|
||||||
|
$testMessages = [
|
||||||
|
['role' => 'user', 'content' => 'Fociz connectiviteitstest. Reageer enkel met het woord OK.'],
|
||||||
];
|
];
|
||||||
} else {
|
|
||||||
// Anders haal de geconfigureerde instellingen op van user of org
|
|
||||||
$testSettings = get_ai_settings($user['id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($testSettings['ai_api_key'])) {
|
if ($provider === 'gemini' || strpos(strtolower($model), 'gemini') !== false) {
|
||||||
|
$aiRes = fociz_ai_chat_gemini($testMessages, ['max_tokens' => 20, 'temperature' => 0.1], $testSettings);
|
||||||
|
} else {
|
||||||
|
$aiRes = fociz_ai_chat_openai($testMessages, ['max_tokens' => 20, 'temperature' => 0.1], $testSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
$latency = round((microtime(true) - $t0) * 1000);
|
||||||
|
|
||||||
|
if (!$aiRes['success']) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $aiRes['error'] ?? 'Verbindingstest mislukt.',
|
||||||
|
'latency_ms' => $latency,
|
||||||
|
'provider' => $provider,
|
||||||
|
'model' => $model,
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$providerName = ($provider === 'gemini' || strpos(strtolower($model), 'gemini') !== false) ? 'Google Gemini Pro' : 'OpenAI';
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => "Verbinding met {$providerName} is succesvol tot stand gebracht! (Response tijd: {$latency}ms)",
|
||||||
|
'latency_ms' => $latency,
|
||||||
|
'provider' => $provider,
|
||||||
|
'model' => $aiRes['model'] ?? $model,
|
||||||
|
'zero_leak' => true,
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
} catch (Throwable $testEx) {
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'error' => 'Geen API-sleutel ingevuld of geconfigureerd om te testen.',
|
'error' => 'Fout tijdens verbindingstest: ' . $testEx->getMessage(),
|
||||||
]);
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$provider = strtolower($testSettings['ai_provider'] ?? 'gemini');
|
|
||||||
$model = $testSettings['ai_model'] ?? ($provider === 'openai' ? 'gpt-4o-mini' : 'gemini-1.5-pro');
|
|
||||||
|
|
||||||
$t0 = microtime(true);
|
|
||||||
|
|
||||||
$testMessages = [
|
|
||||||
['role' => 'user', 'content' => 'Fociz connectiviteitstest. Reageer enkel met het woord OK.'],
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($provider === 'gemini' || strpos(strtolower($model), 'gemini') !== false) {
|
|
||||||
$aiRes = fociz_ai_chat_gemini($testMessages, ['max_tokens' => 20, 'temperature' => 0.1], $testSettings);
|
|
||||||
} else {
|
|
||||||
$aiRes = fociz_ai_chat_openai($testMessages, ['max_tokens' => 20, 'temperature' => 0.1], $testSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
$latency = round((microtime(true) - $t0) * 1000);
|
|
||||||
|
|
||||||
if (!$aiRes['success']) {
|
|
||||||
echo json_encode([
|
|
||||||
'success' => false,
|
|
||||||
'error' => $aiRes['error'] ?? 'Verbindingstest mislukt.',
|
|
||||||
'latency_ms' => $latency,
|
|
||||||
'provider' => $provider,
|
|
||||||
'model' => $model,
|
|
||||||
]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$providerName = ($provider === 'gemini' || strpos(strtolower($model), 'gemini') !== false) ? 'Google Gemini Pro' : 'OpenAI';
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
'success' => true,
|
|
||||||
'message' => "Verbinding met {$providerName} is succesvol tot stand gebracht! (Response tijd: {$latency}ms)",
|
|
||||||
'latency_ms' => $latency,
|
|
||||||
'provider' => $provider,
|
|
||||||
'model' => $aiRes['model'] ?? $model,
|
|
||||||
'zero_leak' => true,
|
|
||||||
]);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Slimme dagindeling berekenen voor taken rondom bezette tijdblokken
|
// 4. Slimme dagindeling berekenen voor taken rondom bezette tijdblokken
|
||||||
@@ -599,7 +614,11 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode(['error' => 'Onbekende actie']);
|
echo json_encode(['error' => 'Onbekende actie']);
|
||||||
} catch (Exception $e) {
|
} catch (Throwable $e) {
|
||||||
|
error_log("api/ai.php Throwable: " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(['error' => $e->getMessage()]);
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Serverfout bij AI verwerking: ' . $e->getMessage()
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -172,6 +172,9 @@ function ensure_tables_exist(PDO $pdo): void {
|
|||||||
"CREATE TABLE IF NOT EXISTS `audit_logs` (`id` INT AUTO_INCREMENT PRIMARY KEY, `user_id` INT NULL, `user_email` VARCHAR(191) NULL, `action` VARCHAR(64) NOT NULL, `resource_type` VARCHAR(64) NULL, `resource_id` INT NULL, `details` TEXT NULL, `ip_address` VARCHAR(45) NOT NULL, `user_agent` VARCHAR(255) NULL, `status` ENUM('SUCCESS', 'FAILED', 'WARNING', 'PENDING') NOT NULL DEFAULT 'SUCCESS', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX (`user_id`), INDEX (`action`), INDEX (`created_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
"CREATE TABLE IF NOT EXISTS `audit_logs` (`id` INT AUTO_INCREMENT PRIMARY KEY, `user_id` INT NULL, `user_email` VARCHAR(191) NULL, `action` VARCHAR(64) NOT NULL, `resource_type` VARCHAR(64) NULL, `resource_id` INT NULL, `details` TEXT NULL, `ip_address` VARCHAR(45) NOT NULL, `user_agent` VARCHAR(255) NULL, `status` ENUM('SUCCESS', 'FAILED', 'WARNING', 'PENDING') NOT NULL DEFAULT 'SUCCESS', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX (`user_id`), INDEX (`action`), INDEX (`created_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||||
// Agenda & Verlof: Hele of meerdere dagen blokkeren (Vakantie, Vrije dag, Ziekte)
|
// Agenda & Verlof: Hele of meerdere dagen blokkeren (Vakantie, Vrije dag, Ziekte)
|
||||||
"CREATE TABLE IF NOT EXISTS `calendar_blocks` (`id` INT AUTO_INCREMENT PRIMARY KEY, `user_id` INT NOT NULL, `start_date` DATE NOT NULL, `end_date` DATE NOT NULL, `title` VARCHAR(255) NOT NULL, `block_type` VARCHAR(50) NOT NULL DEFAULT 'VACATION', `notes` TEXT NULL, `outlook_event_id` VARCHAR(255) NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX (`user_id`, `start_date`, `end_date`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
"CREATE TABLE IF NOT EXISTS `calendar_blocks` (`id` INT AUTO_INCREMENT PRIMARY KEY, `user_id` INT NOT NULL, `start_date` DATE NOT NULL, `end_date` DATE NOT NULL, `title` VARCHAR(255) NOT NULL, `block_type` VARCHAR(50) NOT NULL DEFAULT 'VACATION', `notes` TEXT NULL, `outlook_event_id` VARCHAR(255) NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX (`user_id`, `start_date`, `end_date`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||||
|
// Centrale Systeeminstellingen (AI, Werkdagen, Sessiebeveiliging)
|
||||||
|
"CREATE TABLE IF NOT EXISTS `system_settings` (`setting_key` VARCHAR(64) PRIMARY KEY, `setting_value` LONGTEXT NULL, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||||
|
"ALTER TABLE `system_settings` MODIFY COLUMN `setting_value` LONGTEXT NULL",
|
||||||
];
|
];
|
||||||
foreach ($colUpgrades as $sql) {
|
foreach ($colUpgrades as $sql) {
|
||||||
try {
|
try {
|
||||||
@@ -245,8 +248,8 @@ function ensure_tables_exist(PDO $pdo): void {
|
|||||||
// Pas bestaande audit logs eenmalig aan zodat de weergave klopt met de lokale tijd
|
// Pas bestaande audit logs eenmalig aan zodat de weergave klopt met de lokale tijd
|
||||||
$pdo->exec("UPDATE audit_logs SET created_at = DATE_ADD(created_at, INTERVAL 2 HOUR)");
|
$pdo->exec("UPDATE audit_logs SET created_at = DATE_ADD(created_at, INTERVAL 2 HOUR)");
|
||||||
$pdo->prepare("
|
$pdo->prepare("
|
||||||
INSERT INTO system_settings (setting_key, setting_value, created_at, updated_at)
|
INSERT INTO system_settings (setting_key, setting_value, updated_at)
|
||||||
VALUES ('audit_logs_tz_fixed', '1', NOW(), NOW())
|
VALUES ('audit_logs_tz_fixed', '1', NOW())
|
||||||
ON DUPLICATE KEY UPDATE setting_value = '1', updated_at = NOW()
|
ON DUPLICATE KEY UPDATE setting_value = '1', updated_at = NOW()
|
||||||
")->execute();
|
")->execute();
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-9
@@ -82,12 +82,19 @@ function fociz_ai_enforce_privacy_firewall(array &$messages): array {
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings): array {
|
function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings): array {
|
||||||
$apiKey = $aiSettings['ai_api_key'];
|
$apiKey = trim((string)($aiSettings['ai_api_key'] ?? ''));
|
||||||
$model = !empty($aiSettings['ai_model']) ? $aiSettings['ai_model'] : 'gemini-1.5-pro';
|
$model = !empty($aiSettings['ai_model']) ? trim((string)$aiSettings['ai_model']) : 'gemini-1.5-pro';
|
||||||
$temperature = $options['temperature'] ?? 0.3;
|
$temperature = $options['temperature'] ?? 0.3;
|
||||||
$maxTokens = $options['max_tokens'] ?? 2048;
|
$maxTokens = $options['max_tokens'] ?? 2048;
|
||||||
$jsonMode = !empty($options['json_mode']);
|
$jsonMode = !empty($options['json_mode']);
|
||||||
|
|
||||||
|
if (empty($apiKey)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Geen Google Gemini API-sleutel ingevuld of geconfigureerd.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Google Generative Language API structuur opbouwen
|
// Google Generative Language API structuur opbouwen
|
||||||
$systemInstructions = [];
|
$systemInstructions = [];
|
||||||
$contents = [];
|
$contents = [];
|
||||||
@@ -131,7 +138,8 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings
|
|||||||
$payload['generationConfig']['responseMimeType'] = 'application/json';
|
$payload['generationConfig']['responseMimeType'] = 'application/json';
|
||||||
}
|
}
|
||||||
|
|
||||||
$endpoint = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent";
|
// Geef de sleutel zowel in de URL (?key=) als in de x-goog-api-key header mee voor proxy-ondersteuning
|
||||||
|
$endpoint = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key=" . urlencode($apiKey);
|
||||||
|
|
||||||
$response = fociz_http_request($endpoint, [
|
$response = fociz_http_request($endpoint, [
|
||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
@@ -140,11 +148,20 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings
|
|||||||
'x-goog-api-key' => $apiKey,
|
'x-goog-api-key' => $apiKey,
|
||||||
],
|
],
|
||||||
'body' => $payload,
|
'body' => $payload,
|
||||||
'timeout' => 45,
|
'timeout' => 20,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($response['status'] !== 200 || empty($response['json']['candidates'][0]['content']['parts'][0]['text'])) {
|
if ($response['status'] !== 200 || empty($response['json']['candidates'][0]['content']['parts'][0]['text'])) {
|
||||||
$errMsg = $response['json']['error']['message'] ?? ($response['error'] ?? "Gemini API aanroep mislukt (HTTP {$response['status']})");
|
$errMsg = null;
|
||||||
|
if (!empty($response['json']['error']['message'])) {
|
||||||
|
$errMsg = $response['json']['error']['message'];
|
||||||
|
} elseif (!empty($response['error'])) {
|
||||||
|
$errMsg = $response['error'];
|
||||||
|
} elseif (!empty($response['body'])) {
|
||||||
|
$errMsg = "Gemini fout (HTTP {$response['status']}): " . strip_tags(substr((string)$response['body'], 0, 200));
|
||||||
|
} else {
|
||||||
|
$errMsg = "Gemini API aanroep mislukt (HTTP {$response['status']})";
|
||||||
|
}
|
||||||
error_log("fociz_ai_chat_gemini fout: {$errMsg}");
|
error_log("fociz_ai_chat_gemini fout: {$errMsg}");
|
||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
@@ -188,12 +205,19 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
function fociz_ai_chat_openai(array $messages, array $options, array $aiSettings): array {
|
function fociz_ai_chat_openai(array $messages, array $options, array $aiSettings): array {
|
||||||
$apiKey = $aiSettings['ai_api_key'];
|
$apiKey = trim((string)($aiSettings['ai_api_key'] ?? ''));
|
||||||
$model = !empty($aiSettings['ai_model']) ? $aiSettings['ai_model'] : 'gpt-4o-mini';
|
$model = !empty($aiSettings['ai_model']) ? trim((string)$aiSettings['ai_model']) : 'gpt-4o-mini';
|
||||||
$temperature = $options['temperature'] ?? 0.3;
|
$temperature = $options['temperature'] ?? 0.3;
|
||||||
$maxTokens = $options['max_tokens'] ?? 1500;
|
$maxTokens = $options['max_tokens'] ?? 1500;
|
||||||
$jsonMode = !empty($options['json_mode']);
|
$jsonMode = !empty($options['json_mode']);
|
||||||
|
|
||||||
|
if (empty($apiKey)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Geen OpenAI API-sleutel ingevuld of geconfigureerd.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'model' => $model,
|
'model' => $model,
|
||||||
'messages' => $messages,
|
'messages' => $messages,
|
||||||
@@ -212,11 +236,20 @@ function fociz_ai_chat_openai(array $messages, array $options, array $aiSettings
|
|||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
],
|
],
|
||||||
'body' => $payload,
|
'body' => $payload,
|
||||||
'timeout' => 35,
|
'timeout' => 20,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($response['status'] !== 200 || empty($response['json']['choices'][0]['message']['content'])) {
|
if ($response['status'] !== 200 || empty($response['json']['choices'][0]['message']['content'])) {
|
||||||
$errMsg = $response['json']['error']['message'] ?? ($response['error'] ?? "OpenAI aanroep mislukt (HTTP {$response['status']})");
|
$errMsg = null;
|
||||||
|
if (!empty($response['json']['error']['message'])) {
|
||||||
|
$errMsg = $response['json']['error']['message'];
|
||||||
|
} elseif (!empty($response['error'])) {
|
||||||
|
$errMsg = $response['error'];
|
||||||
|
} elseif (!empty($response['body'])) {
|
||||||
|
$errMsg = "OpenAI fout (HTTP {$response['status']}): " . strip_tags(substr((string)$response['body'], 0, 200));
|
||||||
|
} else {
|
||||||
|
$errMsg = "OpenAI API aanroep mislukt (HTTP {$response['status']})";
|
||||||
|
}
|
||||||
error_log("fociz_ai_chat_openai fout: {$errMsg}");
|
error_log("fociz_ai_chat_openai fout: {$errMsg}");
|
||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
|
|||||||
+2
-2
@@ -34,7 +34,7 @@ function is_https_request(): bool {
|
|||||||
/**
|
/**
|
||||||
* AES-256-GCM / AES-256-CBC encryptie helper voor gevoelige tokens (OAuth, API keys)
|
* AES-256-GCM / AES-256-CBC encryptie helper voor gevoelige tokens (OAuth, API keys)
|
||||||
*/
|
*/
|
||||||
function fociz_encrypt(string $plaintext): string {
|
function fociz_encrypt(?string $plaintext): string {
|
||||||
if (empty($plaintext)) return '';
|
if (empty($plaintext)) return '';
|
||||||
$key = hash('sha256', defined('APP_SECRET') ? APP_SECRET : 'fociz-maria-db-super-secret-key-998877', true);
|
$key = hash('sha256', defined('APP_SECRET') ? APP_SECRET : 'fociz-maria-db-super-secret-key-998877', true);
|
||||||
$iv = random_bytes(16);
|
$iv = random_bytes(16);
|
||||||
@@ -46,7 +46,7 @@ function fociz_encrypt(string $plaintext): string {
|
|||||||
/**
|
/**
|
||||||
* AES-256-CBC decryptie helper; decodeert indien versleuteld, laat legacy plaintext ongemoeid
|
* AES-256-CBC decryptie helper; decodeert indien versleuteld, laat legacy plaintext ongemoeid
|
||||||
*/
|
*/
|
||||||
function fociz_decrypt(string $ciphertext): string {
|
function fociz_decrypt(?string $ciphertext): string {
|
||||||
if (empty($ciphertext)) return '';
|
if (empty($ciphertext)) return '';
|
||||||
if (!str_starts_with($ciphertext, 'ENC:')) {
|
if (!str_starts_with($ciphertext, 'ENC:')) {
|
||||||
return $ciphertext; // Plaintext legacy waarde
|
return $ciphertext; // Plaintext legacy waarde
|
||||||
|
|||||||
+37
-13
@@ -45,8 +45,17 @@ function get_all_system_settings(bool $forceRefresh = false): array {
|
|||||||
$stmt = $db->query("SELECT setting_key, setting_value FROM system_settings");
|
$stmt = $db->query("SELECT setting_key, setting_value FROM system_settings");
|
||||||
while ($row = $stmt->fetch()) {
|
while ($row = $stmt->fetch()) {
|
||||||
$val = $row['setting_value'];
|
$val = $row['setting_value'];
|
||||||
$decoded = json_decode($val, true);
|
if ($val === null) {
|
||||||
$settingsCache[$row['setting_key']] = (json_last_error() === JSON_ERROR_NONE) ? $decoded : $val;
|
$settingsCache[$row['setting_key']] = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$trimmed = trim((string)$val);
|
||||||
|
if (str_starts_with($trimmed, '{') || str_starts_with($trimmed, '[')) {
|
||||||
|
$decoded = json_decode($val, true);
|
||||||
|
$settingsCache[$row['setting_key']] = (json_last_error() === JSON_ERROR_NONE) ? $decoded : $val;
|
||||||
|
} else {
|
||||||
|
$settingsCache[$row['setting_key']] = $val;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
error_log("get_all_system_settings error: " . $e->getMessage());
|
error_log("get_all_system_settings error: " . $e->getMessage());
|
||||||
@@ -60,7 +69,10 @@ function get_all_system_settings(bool $forceRefresh = false): array {
|
|||||||
*/
|
*/
|
||||||
function get_system_setting(string $key, $default = null) {
|
function get_system_setting(string $key, $default = null) {
|
||||||
$all = get_all_system_settings();
|
$all = get_all_system_settings();
|
||||||
return array_key_exists($key, $all) ? $all[$key] : $default;
|
if (array_key_exists($key, $all) && $all[$key] !== null && $all[$key] !== '') {
|
||||||
|
return $all[$key];
|
||||||
|
}
|
||||||
|
return $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,6 +135,8 @@ function get_ai_settings(?int $userId = null): array {
|
|||||||
|
|
||||||
$apiKey = '';
|
$apiKey = '';
|
||||||
$isPersonal = false;
|
$isPersonal = false;
|
||||||
|
$personalProvider = null;
|
||||||
|
$personalModel = null;
|
||||||
|
|
||||||
// 1. Controleer persoonlijke sleutel van de ingelogde gebruiker
|
// 1. Controleer persoonlijke sleutel van de ingelogde gebruiker
|
||||||
if ($userId !== null && $userId > 0) {
|
if ($userId !== null && $userId > 0) {
|
||||||
@@ -134,12 +148,12 @@ function get_ai_settings(?int $userId = null): array {
|
|||||||
if ($row) {
|
if ($row) {
|
||||||
// Sleutel kan in access_token staan of in settings['api_key']
|
// Sleutel kan in access_token staan of in settings['api_key']
|
||||||
if (!empty($row['access_token'])) {
|
if (!empty($row['access_token'])) {
|
||||||
$apiKey = fociz_decrypt($row['access_token']);
|
$apiKey = fociz_decrypt((string)$row['access_token']);
|
||||||
$isPersonal = true;
|
$isPersonal = true;
|
||||||
} elseif (!empty($row['settings'])) {
|
} elseif (!empty($row['settings'])) {
|
||||||
$userSettings = json_decode($row['settings'], true) ?: [];
|
$userSettings = json_decode($row['settings'], true) ?: [];
|
||||||
if (!empty($userSettings['api_key'])) {
|
if (!empty($userSettings['api_key'])) {
|
||||||
$apiKey = fociz_decrypt($userSettings['api_key']);
|
$apiKey = fociz_decrypt((string)$userSettings['api_key']);
|
||||||
$isPersonal = true;
|
$isPersonal = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,10 +161,10 @@ function get_ai_settings(?int $userId = null): array {
|
|||||||
if (!empty($row['settings'])) {
|
if (!empty($row['settings'])) {
|
||||||
$userSettings = json_decode($row['settings'], true) ?: [];
|
$userSettings = json_decode($row['settings'], true) ?: [];
|
||||||
if (!empty($userSettings['provider'])) {
|
if (!empty($userSettings['provider'])) {
|
||||||
$personalProvider = $userSettings['provider'];
|
$personalProvider = (string)$userSettings['provider'];
|
||||||
}
|
}
|
||||||
if (!empty($userSettings['model'])) {
|
if (!empty($userSettings['model'])) {
|
||||||
$personalModel = $userSettings['model'];
|
$personalModel = (string)$userSettings['model'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,24 +177,34 @@ function get_ai_settings(?int $userId = null): array {
|
|||||||
if (empty($apiKey)) {
|
if (empty($apiKey)) {
|
||||||
$savedKeyRaw = get_system_setting('ai_api_key', '');
|
$savedKeyRaw = get_system_setting('ai_api_key', '');
|
||||||
if (!empty($savedKeyRaw)) {
|
if (!empty($savedKeyRaw)) {
|
||||||
$apiKey = fociz_decrypt($savedKeyRaw);
|
$apiKey = fociz_decrypt((string)$savedKeyRaw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fallback naar .env variabelen (GEMINI_API_KEY of OPENAI_API_KEY)
|
// 3. Fallback naar .env variabelen (GEMINI_API_KEY of OPENAI_API_KEY)
|
||||||
if (empty($apiKey)) {
|
if (empty($apiKey)) {
|
||||||
$apiKey = fociz_env('GEMINI_API_KEY', '') ?: fociz_env('OPENAI_API_KEY', '');
|
$apiKey = (string)(fociz_env('GEMINI_API_KEY', '') ?: fociz_env('OPENAI_API_KEY', ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
$provider = !empty($personalProvider) ? $personalProvider : get_system_setting('ai_provider', 'gemini');
|
$provider = !empty($personalProvider) ? $personalProvider : get_system_setting('ai_provider', 'gemini');
|
||||||
|
if (empty($provider)) {
|
||||||
|
$provider = 'gemini';
|
||||||
|
}
|
||||||
|
|
||||||
$model = !empty($personalModel) ? $personalModel : get_system_setting('ai_model', 'gemini-1.5-pro');
|
$model = !empty($personalModel) ? $personalModel : get_system_setting('ai_model', 'gemini-1.5-pro');
|
||||||
|
if (empty($model)) {
|
||||||
|
$model = (strpos((string)$provider, 'openai') !== false) ? 'gpt-4o-mini' : 'gemini-1.5-pro';
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = trim((string)$apiKey);
|
||||||
|
$hasKey = ($apiKey !== '');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'ai_enabled' => (bool)get_system_setting('ai_enabled', !empty($apiKey)),
|
'ai_enabled' => (bool)get_system_setting('ai_enabled', $hasKey),
|
||||||
'ai_provider' => $provider,
|
'ai_provider' => (string)$provider,
|
||||||
'ai_api_key' => $apiKey,
|
'ai_api_key' => $apiKey,
|
||||||
'ai_model' => $model,
|
'ai_model' => (string)$model,
|
||||||
'has_key' => !empty($apiKey),
|
'has_key' => $hasKey,
|
||||||
'is_personal_key'=> $isPersonal,
|
'is_personal_key'=> $isPersonal,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user