diff --git a/admin.php b/admin.php index e850f72..83959f0 100644 --- a/admin.php +++ b/admin.php @@ -41,6 +41,37 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { header('Location: /admin.php'); exit; } + if ($action === 'save-openai-api-key') { + if (!diagnostic_admin_is_authenticated()) { + diagnostic_flash_set('warning', 'Zaloguj się jako administrator, aby zapisać klucz OpenAI.'); + header('Location: /admin.php'); + exit; + } + + $openAiApiKey = preg_replace('/\s+/', '', (string)($_POST['openai_api_key'] ?? '')) ?? ''; + if (!diagnostic_openai_api_key_is_valid($openAiApiKey)) { + diagnostic_flash_set('danger', 'Podaj poprawny klucz API OpenAI. Klucz powinien być kompletny i bez spacji.'); + header('Location: /admin.php'); + exit; + } + + diagnostic_admin_setting_set('openai_api_key', $openAiApiKey); + diagnostic_flash_set('success', 'Własny klucz API OpenAI został zapisany. Nowe zapytania AI będą używały tego klucza.'); + header('Location: /admin.php'); + exit; + } + if ($action === 'remove-openai-api-key') { + if (!diagnostic_admin_is_authenticated()) { + diagnostic_flash_set('warning', 'Zaloguj się jako administrator, aby usunąć klucz OpenAI.'); + header('Location: /admin.php'); + exit; + } + + diagnostic_admin_setting_set('openai_api_key', null); + diagnostic_flash_set('info', 'Zapisany klucz API OpenAI został usunięty. System wrócił do domyślnej konfiguracji proxy.'); + header('Location: /admin.php'); + exit; + } if ($action === 'resend-report') { if (!diagnostic_admin_is_authenticated()) { diagnostic_flash_set('warning', 'Zaloguj się jako administrator, aby ponownie wysłać raport.'); @@ -91,6 +122,7 @@ if (!$selectedAttempt && !empty($attempts)) { $questionMap = diagnostic_question_map(); $credentials = diagnostic_admin_credentials(); $notificationConfig = diagnostic_admin_is_authenticated() ? diagnostic_admin_notification_config() : ['configured_email' => '', 'fallback_email' => '', 'effective_email' => '', 'source' => 'none']; +$openAiConfig = diagnostic_admin_is_authenticated() ? diagnostic_admin_openai_key_config() : ['configured' => false, 'masked_key' => '']; ?> @@ -183,36 +215,81 @@ $notificationConfig = diagnostic_admin_is_authenticated() ? diagnostic_admin_not
%Średni wynik
-
-
-
-
Powiadomienia o konsultacji
-

Adres administratora dla nowych numerów telefonu

-

Gdy użytkownik poda numer telefonu po ukończeniu diagnozy, system wyśle powiadomienie e-mail właśnie na ten adres.

-
- -
- - +
+
+
+
+
+
Powiadomienia o konsultacji
+

Adres administratora dla nowych numerów telefonu

+

Gdy użytkownik poda numer telefonu po ukończeniu diagnozy, system wyśle powiadomienie e-mail właśnie na ten adres.

+ + +
+ + +
+
+ +
+
-
- -
- -
-
-
- Aktywna konfiguracja +
+
+ Aktywna konfiguracja - Powiadomienia trafiają na: - Źródło: ustawienie zapisane w panelu administracyjnym. + Powiadomienia trafiają na: + Źródło: ustawienie zapisane w panelu administracyjnym. - Powiadomienia tymczasowo trafią na: - To fallback testowy z MAIL_TO. Ustaw adres w panelu, aby zarządzać nim bez edycji środowiska. + Powiadomienia tymczasowo trafią na: + To fallback testowy z MAIL_TO. Ustaw adres w panelu, aby zarządzać nim bez edycji środowiska. - Brak ustawionego adresu do powiadomień. - Do czasu zapisania adresu w panelu e-mail o nowej prośbie o kontakt nie zostanie wysłany. + Brak ustawionego adresu do powiadomień. + Do czasu zapisania adresu w panelu e-mail o nowej prośbie o kontakt nie zostanie wysłany. +
+
+
+
+
+
+
+
+
+
Integracja AI
+

Własny klucz API OpenAI

+

Zapisany tutaj klucz będzie automatycznie używany przez wszystkie obecne wywołania AI w aplikacji: diagnozę, czat i webhook Telegrama.

+
+ +
+ + +
Pole pozostaje puste po zapisaniu ze względów bezpieczeństwa. Wprowadź nowy klucz tylko wtedy, gdy chcesz go zmienić.
+
+
+ +
+
+ +
+ + +
+ +
+
+
+ Aktywna konfiguracja + + Zapytania AI używają własnego klucza zapisanego w panelu. + Zapisany klucz: + Klucz jest wysyłany centralnie przez LocalAIApi, więc nie trzeba osobno zmieniać endpointów. + + Brak własnego klucza OpenAI w panelu. + Do czasu zapisania klucza aplikacja korzysta z domyślnej konfiguracji proxy. + +
+
diff --git a/ai/LocalAIApi.php b/ai/LocalAIApi.php index d428248..7151194 100644 --- a/ai/LocalAIApi.php +++ b/ai/LocalAIApi.php @@ -28,6 +28,11 @@ class LocalAIApi /** @var array|null */ private static ?array $configCache = null; + /** @var string|null */ + private static ?string $customApiKeyCache = null; + + private static bool $customApiKeyLoaded = false; + /** * Signature compatible with the OpenAI Responses API. * @@ -150,6 +155,7 @@ class LocalAIApi } } } + $headers = self::withCustomAuthorizationHeader($headers); if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) { $payload['project_uuid'] = $projectUuid; @@ -273,6 +279,7 @@ class LocalAIApi } } } + $headers = self::withCustomAuthorizationHeader($headers); return self::sendCurl($url, 'GET', null, $headers, $timeout, $verifyTls); } @@ -365,6 +372,71 @@ class LocalAIApi return self::$configCache; } + /** + * Append the admin-provided Authorization header when a custom OpenAI key is configured. + * + * @param array $headers + * @return array + */ + private static function withCustomAuthorizationHeader(array $headers): array + { + $customApiKey = self::customApiKey(); + if ($customApiKey === null) { + return $headers; + } + + foreach ($headers as $index => $header) { + if (stripos($header, 'Authorization:') === 0) { + unset($headers[$index]); + } + } + + $headers[] = 'Authorization: Bearer ' . $customApiKey; + return array_values($headers); + } + + private static function customApiKey(): ?string + { + if (self::$customApiKeyLoaded) { + return self::$customApiKeyCache; + } + + self::$customApiKeyLoaded = true; + self::$customApiKeyCache = null; + + $dbConfigPath = __DIR__ . '/../db/config.php'; + if (!function_exists('db')) { + if (!file_exists($dbConfigPath)) { + return null; + } + require_once $dbConfigPath; + } + + if (!function_exists('db')) { + return null; + } + + try { + $pdo = db(); + $stmt = $pdo->prepare('SELECT setting_value FROM diagnostic_settings WHERE setting_key = :setting_key LIMIT 1'); + $stmt->execute([':setting_key' => 'openai_api_key']); + $value = $stmt->fetchColumn(); + if (!is_string($value)) { + return null; + } + + $normalized = preg_replace('/\s+/', '', trim($value)) ?? ''; + if ($normalized === '' || strlen($normalized) < 20) { + return null; + } + + self::$customApiKeyCache = $normalized; + return self::$customApiKeyCache; + } catch (Throwable $exception) { + return null; + } + } + /** * Build an absolute URL from base_url and a path. */ diff --git a/app/diagnostic_functions.php b/app/diagnostic_functions.php index e32d9ee..08b5567 100644 --- a/app/diagnostic_functions.php +++ b/app/diagnostic_functions.php @@ -1042,6 +1042,58 @@ function diagnostic_admin_notification_email(): ?string return $config['effective_email'] !== '' ? $config['effective_email'] : null; } +function diagnostic_mask_secret(string $value, int $visibleTail = 4): string +{ + $normalized = preg_replace('/\s+/', '', trim($value)) ?? ''; + if ($normalized === '') { + return ''; + } + + $length = function_exists('mb_strlen') ? mb_strlen($normalized) : strlen($normalized); + if ($length <= $visibleTail) { + return str_repeat('•', max($length, 8)); + } + + $tail = function_exists('mb_substr') ? mb_substr($normalized, -$visibleTail) : substr($normalized, -$visibleTail); + return str_repeat('•', max($length - $visibleTail, 8)) . $tail; +} + +function diagnostic_openai_api_key_is_valid(string $value): bool +{ + $normalized = preg_replace('/\s+/', '', trim($value)) ?? ''; + if ($normalized === '' || strlen($normalized) < 20) { + return false; + } + + return preg_match('/^[A-Za-z0-9_\-]+$/', $normalized) === 1; +} + +function diagnostic_admin_openai_key_config(): array +{ + $configuredKey = diagnostic_admin_setting_get('openai_api_key') ?? ''; + $configuredKey = preg_replace('/\s+/', '', $configuredKey) ?? ''; + if ($configuredKey !== '' && !diagnostic_openai_api_key_is_valid($configuredKey)) { + $configuredKey = ''; + } + + return [ + 'configured' => $configuredKey !== '', + 'masked_key' => $configuredKey !== '' ? diagnostic_mask_secret($configuredKey) : '', + ]; +} + +function diagnostic_admin_openai_api_key(): ?string +{ + $config = diagnostic_admin_openai_key_config(); + if (empty($config['configured'])) { + return null; + } + + $key = diagnostic_admin_setting_get('openai_api_key') ?? ''; + $key = preg_replace('/\s+/', '', $key) ?? ''; + return $key !== '' ? $key : null; +} + function diagnostic_send_consultation_notification(array $attempt): array { $recipient = diagnostic_admin_notification_email();