Własny klucz API
This commit is contained in:
parent
eefa4c490d
commit
12a4fd9735
127
admin.php
127
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' => ''];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
@ -183,36 +215,81 @@ $notificationConfig = diagnostic_admin_is_authenticated() ? diagnostic_admin_not
|
||||
<div class="col-sm-6 col-lg-3"><div class="metric-card"><strong><?= (int)$stats['average_score'] ?>%</strong><span>Średni wynik</span></div></div>
|
||||
</section>
|
||||
|
||||
<section class="surface-card p-4 mb-4">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-7">
|
||||
<div class="eyebrow mb-2">Powiadomienia o konsultacji</div>
|
||||
<h2 class="h4 mb-2">Adres administratora dla nowych numerów telefonu</h2>
|
||||
<p class="text-secondary mb-3">Gdy użytkownik poda numer telefonu po ukończeniu diagnozy, system wyśle powiadomienie e-mail właśnie na ten adres.</p>
|
||||
<form method="post" class="row g-3" novalidate>
|
||||
<input type="hidden" name="action" value="save-notification-email">
|
||||
<div class="col-md-8 col-lg-7">
|
||||
<label for="notificationEmail" class="form-label">Adres e-mail administratora</label>
|
||||
<input type="email" class="form-control" id="notificationEmail" name="notification_email" placeholder="np. kontakt@twojafirma.pl" value="<?= htmlspecialchars((string)$notificationConfig['configured_email']) ?>" required>
|
||||
<section class="row g-4 mb-4">
|
||||
<div class="col-xl-6">
|
||||
<div class="surface-card p-4 h-100">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-7">
|
||||
<div class="eyebrow mb-2">Powiadomienia o konsultacji</div>
|
||||
<h2 class="h4 mb-2">Adres administratora dla nowych numerów telefonu</h2>
|
||||
<p class="text-secondary mb-3">Gdy użytkownik poda numer telefonu po ukończeniu diagnozy, system wyśle powiadomienie e-mail właśnie na ten adres.</p>
|
||||
<form method="post" class="row g-3" novalidate>
|
||||
<input type="hidden" name="action" value="save-notification-email">
|
||||
<div class="col-md-8">
|
||||
<label for="notificationEmail" class="form-label">Adres e-mail administratora</label>
|
||||
<input type="email" class="form-control" id="notificationEmail" name="notification_email" placeholder="np. kontakt@twojafirma.pl" value="<?= htmlspecialchars((string)$notificationConfig['configured_email']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-4 d-grid align-self-end">
|
||||
<button type="submit" class="btn btn-dark">Zapisz adres</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-4 col-lg-3 d-grid align-self-end">
|
||||
<button type="submit" class="btn btn-dark">Zapisz adres</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="compact-card h-100">
|
||||
<strong>Aktywna konfiguracja</strong>
|
||||
<div class="col-lg-5">
|
||||
<div class="compact-card h-100">
|
||||
<strong>Aktywna konfiguracja</strong>
|
||||
<?php if (($notificationConfig['source'] ?? 'none') === 'panel'): ?>
|
||||
<span>Powiadomienia trafiają na: <strong><?= htmlspecialchars((string)$notificationConfig['effective_email']) ?></strong></span>
|
||||
<span class="text-secondary">Źródło: ustawienie zapisane w panelu administracyjnym.</span>
|
||||
<span>Powiadomienia trafiają na: <strong><?= htmlspecialchars((string)$notificationConfig['effective_email']) ?></strong></span>
|
||||
<span class="text-secondary">Źródło: ustawienie zapisane w panelu administracyjnym.</span>
|
||||
<?php elseif (($notificationConfig['source'] ?? 'none') === 'env'): ?>
|
||||
<span>Powiadomienia tymczasowo trafią na: <strong><?= htmlspecialchars((string)$notificationConfig['effective_email']) ?></strong></span>
|
||||
<span class="text-secondary">To fallback testowy z <code>MAIL_TO</code>. Ustaw adres w panelu, aby zarządzać nim bez edycji środowiska.</span>
|
||||
<span>Powiadomienia tymczasowo trafią na: <strong><?= htmlspecialchars((string)$notificationConfig['effective_email']) ?></strong></span>
|
||||
<span class="text-secondary">To fallback testowy z <code>MAIL_TO</code>. Ustaw adres w panelu, aby zarządzać nim bez edycji środowiska.</span>
|
||||
<?php else: ?>
|
||||
<span>Brak ustawionego adresu do powiadomień.</span>
|
||||
<span class="text-secondary">Do czasu zapisania adresu w panelu e-mail o nowej prośbie o kontakt nie zostanie wysłany.</span>
|
||||
<span>Brak ustawionego adresu do powiadomień.</span>
|
||||
<span class="text-secondary">Do czasu zapisania adresu w panelu e-mail o nowej prośbie o kontakt nie zostanie wysłany.</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-6">
|
||||
<div class="surface-card p-4 h-100">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-7">
|
||||
<div class="eyebrow mb-2">Integracja AI</div>
|
||||
<h2 class="h4 mb-2">Własny klucz API OpenAI</h2>
|
||||
<p class="text-secondary mb-3">Zapisany tutaj klucz będzie automatycznie używany przez wszystkie obecne wywołania AI w aplikacji: diagnozę, czat i webhook Telegrama.</p>
|
||||
<form method="post" class="row g-3" novalidate>
|
||||
<input type="hidden" name="action" value="save-openai-api-key">
|
||||
<div class="col-12">
|
||||
<label for="openAiApiKey" class="form-label">Klucz API OpenAI</label>
|
||||
<input type="password" class="form-control" id="openAiApiKey" name="openai_api_key" placeholder="wklej pełny klucz, np. sk-proj-..." value="" autocomplete="new-password" spellcheck="false">
|
||||
<div class="form-text">Pole pozostaje puste po zapisaniu ze względów bezpieczeństwa. Wprowadź nowy klucz tylko wtedy, gdy chcesz go zmienić.</div>
|
||||
</div>
|
||||
<div class="col-sm-6 d-grid">
|
||||
<button type="submit" class="btn btn-dark">Zapisz klucz</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php if (!empty($openAiConfig['configured'])): ?>
|
||||
<form method="post" class="mt-3" onsubmit="return confirm('Usunąć zapisany klucz API OpenAI?');">
|
||||
<input type="hidden" name="action" value="remove-openai-api-key">
|
||||
<button type="submit" class="btn btn-outline-dark">Usuń zapisany klucz</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="compact-card h-100">
|
||||
<strong>Aktywna konfiguracja</strong>
|
||||
<?php if (!empty($openAiConfig['configured'])): ?>
|
||||
<span>Zapytania AI używają własnego klucza zapisanego w panelu.</span>
|
||||
<span class="text-secondary">Zapisany klucz: <strong><?= htmlspecialchars((string)$openAiConfig['masked_key']) ?></strong></span>
|
||||
<span class="text-secondary">Klucz jest wysyłany centralnie przez <code>LocalAIApi</code>, więc nie trzeba osobno zmieniać endpointów.</span>
|
||||
<?php else: ?>
|
||||
<span>Brak własnego klucza OpenAI w panelu.</span>
|
||||
<span class="text-secondary">Do czasu zapisania klucza aplikacja korzysta z domyślnej konfiguracji proxy.</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -28,6 +28,11 @@ class LocalAIApi
|
||||
/** @var array<string,mixed>|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<int,string> $headers
|
||||
* @return array<int,string>
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user