security: protect API keys with backend proxy, harden auth, htaccess and session management

This commit is contained in:
Philipp Urbschat 2026-09-12 22:14:58 +02:00
parent a77a1a62e7
commit cde87db064
Signed by: Phili
SSH key fingerprint: SHA256:ZSQWnldzrYiABzOV6vTICPe0h19pTpus7sCbm2S0po0
21 changed files with 190 additions and 1458 deletions

View file

@ -2,17 +2,26 @@
Options -Indexes Options -Indexes
RewriteEngine On RewriteEngine On
# Interne PHP- und Template-Verzeichnisse vor direktem Webzugriff sperren # HTTP Security Headers
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
# Interne PHP-, Vendor- und Template-Verzeichnisse vor direktem Webzugriff sperren
RewriteRule ^(home|philcore)/(app|core|views|src)/ - [F,L] RewriteRule ^(home|philcore)/(app|core|views|src)/ - [F,L]
RewriteRule ^(.*/)?vendor/ - [F,L]
# Sensible Dateien und Verzeichnisse vor direktem Webzugriff schützen # Sensible Dateien und Verzeichnisse vor direktem Webzugriff schützen
<FilesMatch "(^\.|\.(json|lock|sql|env|md)$)"> <FilesMatch "(^\.|\.(json|lock|sql|env|md)$)">
Require all denied Require all denied
</FilesMatch> </FilesMatch>
<Files "auth.php"> # Interne Hilfsdateien vor direktem Aufruf schützen
<FilesMatch "^(auth\.php|db\.php|google_helper\.php)$">
Require all denied Require all denied
</Files> </FilesMatch>
# 1. Assets aus home/public weiterleiten (falls direkt aufgerufen wie /css/style.css) # 1. Assets aus home/public weiterleiten (falls direkt aufgerufen wie /css/style.css)
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f

View file

@ -6,6 +6,7 @@ if (session_status() === PHP_SESSION_NONE) {
session_set_cookie_params([ session_set_cookie_params([
'path' => '/', 'path' => '/',
'httponly' => true, 'httponly' => true,
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
'samesite' => 'Lax' // Lax erlaubt den Wechsel zwischen /home und /test 'samesite' => 'Lax' // Lax erlaubt den Wechsel zwischen /home und /test
]); ]);
session_start(); session_start();

92
dinos/api_proxy.php Normal file
View file

@ -0,0 +1,92 @@
<?php
// DATEI: dinos/api_proxy.php
$current_project = 'dinos';
require_once __DIR__ . '/../auth.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Nur POST-Anfragen sind erlaubt.']);
exit;
}
// 1. API-Schlüssel sicher aus .env laden
$apiKey = '';
$envFiles = [__DIR__ . '/.env', __DIR__ . '/../home/.env'];
foreach ($envFiles as $envFile) {
if (file_exists($envFile)) {
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') continue;
if (strpos($line, '=') !== false) {
list($k, $v) = explode('=', $line, 2);
if (trim($k) === 'GEMINI_API_KEY') {
$apiKey = trim(trim($v), '"\'');
break 2;
}
}
}
}
}
if (empty($apiKey)) {
http_response_code(500);
echo json_encode(['error' => 'API-Schlüssel nicht konfiguriert.']);
exit;
}
// 2. JSON-Daten empfangen
$json_input = file_get_contents('php://input');
$request_data = json_decode($json_input, true);
if (json_last_error() !== JSON_ERROR_NONE || !isset($request_data['apiUrl']) || !isset($request_data['payload'])) {
http_response_code(400);
echo json_encode(['error' => 'Ungültige Anfrage-Daten.']);
exit;
}
$apiUrl = $request_data['apiUrl'];
$payload = $request_data['payload'];
// 3. SSRF-Schutz: Nur generativelanguage.googleapis.com erlauben
$parsed = parse_url($apiUrl);
if (!isset($parsed['host']) || $parsed['host'] !== 'generativelanguage.googleapis.com') {
http_response_code(403);
echo json_encode(['error' => 'Ungültiges API-Ziel.']);
exit;
}
// 4. Ziel-URL mit API-Schlüssel zusammensetzen
$separator = (strpos($apiUrl, '?') !== false) ? '&' : '?';
$fullApiUrl = $apiUrl . $separator . 'key=' . $apiKey;
// 5. Anfrage mit cURL an Google API senden
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $fullApiUrl,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 90,
]);
$response_body = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
http_response_code(500);
echo json_encode(['error' => 'Fehler bei Weiterleitung an Google: ' . curl_error($ch)]);
exit;
}
curl_close($ch);
http_response_code($http_code);
echo $response_body;
?>

View file

@ -5,6 +5,15 @@
* Führt den Google Login durch und speichert den Token. * Führt den Google Login durch und speichert den Token.
*/ */
$current_project = 'dinos';
require_once __DIR__ . '/../auth.php';
// Nur Administratoren dürfen Google OAuth Konten verknüpfen
if (!isset($_SESSION['is_admin']) || $_SESSION['is_admin'] !== true) {
header('Location: /');
exit();
}
require_once 'google_helper.php'; require_once 'google_helper.php';
// Konfiguration // Konfiguration

View file

@ -16,25 +16,6 @@ $dailyLimitEco = 100; // Eco (Flash) etwa 3-4 Cent pro Bild
$dailyLimitPro = 10; // Pro (Imagen 4) etwa 3-4 Cent pro Bild $dailyLimitPro = 10; // Pro (Imagen 4) etwa 3-4 Cent pro Bild
$dailyLimitUltra = 10; // Ultra (Gemini 3) etwa 12 Cent pro Bild $dailyLimitUltra = 10; // Ultra (Gemini 3) etwa 12 Cent pro Bild
// API-Key sicher aus .env laden
$apiKey = '';
$envFiles = [__DIR__ . '/.env', __DIR__ . '/../home/.env'];
foreach ($envFiles as $envFile) {
if (file_exists($envFile)) {
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') continue;
if (strpos($line, '=') !== false) {
list($k, $v) = explode('=', $line, 2);
if (trim($k) === 'GEMINI_API_KEY') {
$apiKey = trim(trim($v), '"\'');
break 2;
}
}
}
}
}
// API Modelle (hier zentral ändern, wenn Versionen veralten) siehe apis.php // API Modelle (hier zentral ändern, wenn Versionen veralten) siehe apis.php
$modelText = "gemini-2.5-flash"; // Für Fakten und Zufallsgenerator $modelText = "gemini-2.5-flash"; // Für Fakten und Zufallsgenerator
$modelImageEco = "gemini-2.5-flash-image"; // ECO Modus $modelImageEco = "gemini-2.5-flash-image"; // ECO Modus
@ -316,7 +297,6 @@ $totalUltra = $currentStats['total_ultra'];
<script> <script>
// API Konfiguration ans Frontend übergeben // API Konfiguration ans Frontend übergeben
const apiKey = "<?php echo $apiKey; ?>";
const apiModels = { const apiModels = {
text: "<?php echo $modelText; ?>", text: "<?php echo $modelText; ?>",
imageEco: "<?php echo $modelImageEco; ?>", imageEco: "<?php echo $modelImageEco; ?>",
@ -475,11 +455,6 @@ $totalUltra = $currentStats['total_ultra'];
// --- ZUFALLSGENERATOR LOGIK --- // --- ZUFALLSGENERATOR LOGIK ---
async function handleRandomDino() { async function handleRandomDino() {
if (!apiKey) {
showError("API Key fehlt.");
return;
}
let diceIconElem = document.getElementById('diceIcon'); let diceIconElem = document.getElementById('diceIcon');
if (!diceIconElem) diceIconElem = randomBtn.querySelector('svg'); if (!diceIconElem) diceIconElem = randomBtn.querySelector('svg');
@ -488,21 +463,22 @@ $totalUltra = $currentStats['total_ultra'];
try { try {
const prompt = DinoPrompts.getRandom(); const prompt = DinoPrompts.getRandom();
const response = await fetch( const response = await fetch('api_proxy.php', {
`https://generativelanguage.googleapis.com/v1beta/models/${apiModels.text}:generateContent?key=${apiKey}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
apiUrl: `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.text}:generateContent`,
payload: {
contents: [{ contents: [{
parts: [{ parts: [{
text: prompt text: prompt
}] }]
}] }]
})
} }
); })
});
if (!response.ok) throw new Error("Netzwerkfehler"); if (!response.ok) throw new Error("Netzwerkfehler");
const result = await response.json(); const result = await response.json();
@ -551,10 +527,6 @@ $totalUltra = $currentStats['total_ultra'];
const searchTerm = searchTermInput.value.trim(); const searchTerm = searchTermInput.value.trim();
if (!searchTerm) return; if (!searchTerm) return;
if (!apiKey) {
showError("API Key fehlt.");
return;
}
if (typeof DinoPrompts === 'undefined') { if (typeof DinoPrompts === 'undefined') {
showError("Fehler: prompts.js nicht geladen."); showError("Fehler: prompts.js nicht geladen.");
return; return;
@ -667,21 +639,22 @@ $totalUltra = $currentStats['total_ultra'];
async function fetchDinoFacts(term) { async function fetchDinoFacts(term) {
const factsPrompt = DinoPrompts.getFacts(term); const factsPrompt = DinoPrompts.getFacts(term);
const response = await fetch( const response = await fetch('api_proxy.php', {
`https://generativelanguage.googleapis.com/v1beta/models/${apiModels.text}:generateContent?key=${apiKey}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
apiUrl: `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.text}:generateContent`,
payload: {
contents: [{ contents: [{
parts: [{ parts: [{
text: factsPrompt text: factsPrompt
}] }]
}] }]
})
} }
); })
});
if (!response.ok) { if (!response.ok) {
const errText = await response.text(); const errText = await response.text();
if (response.status === 429 || errText.includes('RESOURCE_EXHAUSTED')) { if (response.status === 429 || errText.includes('RESOURCE_EXHAUSTED')) {
@ -708,12 +681,12 @@ $totalUltra = $currentStats['total_ultra'];
async function fetchDinoImage(dinoData, type, attempt = 1) { async function fetchDinoImage(dinoData, type, attempt = 1) {
const imagePrompt = DinoPrompts.getImage(dinoData); const imagePrompt = DinoPrompts.getImage(dinoData);
let url = ''; let apiUrl = '';
let payload = {}; let payload = {};
let isImagen = false; let isImagen = false;
if (type === 'ultra') { if (type === 'ultra') {
url = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imageUltra}:generateContent?key=${apiKey}`; apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imageUltra}:generateContent`;
payload = { payload = {
contents: [{ contents: [{
parts: [{ parts: [{
@ -725,7 +698,7 @@ $totalUltra = $currentStats['total_ultra'];
} }
}; };
} else if (type === 'pro') { } else if (type === 'pro') {
url = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imagePro}:predict?key=${apiKey}`; apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imagePro}:predict`;
payload = { payload = {
instances: [{ instances: [{
prompt: imagePrompt prompt: imagePrompt
@ -736,7 +709,7 @@ $totalUltra = $currentStats['total_ultra'];
}; };
isImagen = true; isImagen = true;
} else { } else {
url = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imageEco}:generateContent?key=${apiKey}`; apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imageEco}:generateContent`;
payload = { payload = {
contents: [{ contents: [{
parts: [{ parts: [{
@ -750,12 +723,15 @@ $totalUltra = $currentStats['total_ultra'];
} }
try { try {
const response = await fetch(url, { const response = await fetch('api_proxy.php', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify(payload) body: JSON.stringify({
apiUrl: apiUrl,
payload: payload
})
}); });
if (!response.ok) { if (!response.ok) {

File diff suppressed because it is too large Load diff

View file

@ -20,9 +20,11 @@ class LoginController extends Controller {
$userModel = $this->model('User'); $userModel = $this->model('User');
$user = $userModel->authenticate($email, $password); $user = $userModel->authenticate($email, $password);
if ($user) { if ($user) {
session_regenerate_id(true);
$_SESSION['user_email'] = $email; $_SESSION['user_email'] = $email;
$_SESSION['user_projects'] = $user['projects'] ?? []; $_SESSION['user_projects'] = $user['projects'] ?? [];
$_SESSION['is_admin'] = $user['is_admin'] ?? false; $_SESSION['is_admin'] = $user['is_admin'] ?? false;
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
header('Location: ' . Config::get('BASE_URL', '/')); header('Location: ' . Config::get('BASE_URL', '/'));
} else { } else {
Flash::set('Access denied: Invalid credentials.'); Flash::set('Access denied: Invalid credentials.');

View file

@ -4,6 +4,7 @@ if (session_status() === PHP_SESSION_NONE) {
session_set_cookie_params([ session_set_cookie_params([
'path' => '/', 'path' => '/',
'httponly' => true, 'httponly' => true,
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
'samesite' => 'Lax' 'samesite' => 'Lax'
]); ]);
session_start(); session_start();

View file

@ -2,7 +2,7 @@
class Controller { class Controller {
public function model($model) { protected function model($model) {
$modelPath = __DIR__ . '/../app/models/' . $model . '.php'; $modelPath = __DIR__ . '/../app/models/' . $model . '.php';
if (file_exists($modelPath)) { if (file_exists($modelPath)) {
require_once $modelPath; require_once $modelPath;
@ -11,7 +11,7 @@ class Controller {
throw new Exception("Model '{$model}' existiert nicht."); throw new Exception("Model '{$model}' existiert nicht.");
} }
public function view($view, $data = []) { protected function view($view, $data = []) {
if (!empty($data)) { if (!empty($data)) {
extract($data); extract($data);
} }
@ -25,20 +25,20 @@ class Controller {
} }
} }
public function json($data, $status = 200) { protected function json($data, $status = 200) {
http_response_code($status); http_response_code($status);
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode($data); echo json_encode($data);
exit; exit;
} }
public function xml($data) { protected function xml($data) {
header('Content-Type: application/xml; charset=utf-8'); header('Content-Type: application/xml; charset=utf-8');
echo $data; echo $data;
exit; exit;
} }
public function text($data) { protected function text($data) {
header('Content-Type: text/plain; charset=utf-8'); header('Content-Type: text/plain; charset=utf-8');
echo $data; echo $data;
exit; exit;

View file

@ -16,8 +16,9 @@ class Router {
$this->controller = new $this->controller; $this->controller = new $this->controller;
// Method Check // Method Check
if (isset($url[1])) { if (isset($url[1])) {
if (method_exists($this->controller, $url[1])) { $candidateMethod = $url[1];
$this->method = $url[1]; if (is_callable([$this->controller, $candidateMethod]) && strpos($candidateMethod, '_') !== 0) {
$this->method = $candidateMethod;
unset($url[1]); unset($url[1]);
} }
} }

View file

@ -9,7 +9,7 @@ class Security {
} }
// CSRF Validation // CSRF Validation
public static function checkCsrf($token) { public static function checkCsrf($token) {
if (!isset($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) { if (!isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], (string)$token)) {
throw new Exception("Sicherheits-Token ungültig. Bitte lade die Seite neu."); throw new Exception("Sicherheits-Token ungültig. Bitte lade die Seite neu.");
} }
return true; return true;

View file

@ -1,11 +1,12 @@
<?php <?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
if (file_exists(__DIR__ . '/../app/init.php')) { if (file_exists(__DIR__ . '/../app/init.php')) {
require_once __DIR__ . '/../app/init.php'; require_once __DIR__ . '/../app/init.php';
} else { } else {
die("Kritischer Fehler: app/init.php wurde nicht gefunden."); die("Kritischer Fehler: app/init.php wurde nicht gefunden.");
} }
error_reporting(E_ALL);
ini_set('display_errors', Config::get('APP_ENV') === 'development' ? 1 : 0);
try { try {
// Router // Router
$router = new Router(); $router = new Router();

View file

@ -1,6 +1,11 @@
<?php <?php
// 1. Session starten
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_set_cookie_params([
'path' => '/',
'httponly' => true,
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
'samesite' => 'Lax'
]);
session_start(); session_start();
} }

View file

@ -2,7 +2,7 @@
class Controller { class Controller {
// Modell laden // Modell laden
public function model($model) { protected function model($model) {
$modelPath = __DIR__ . '/../app/models/' . $model . '.php'; $modelPath = __DIR__ . '/../app/models/' . $model . '.php';
if (file_exists($modelPath)) { if (file_exists($modelPath)) {
require_once $modelPath; require_once $modelPath;
@ -12,7 +12,7 @@ class Controller {
} }
// View laden // View laden
public function view($view, $data = []) { protected function view($view, $data = []) {
if (!empty($data)) { if (!empty($data)) {
extract($data); extract($data);
} }
@ -28,10 +28,22 @@ class Controller {
} }
// JSON-Response für APIs und AJAX // JSON-Response für APIs und AJAX
public function json($data, $status = 200) { protected function json($data, $status = 200) {
http_response_code($status); http_response_code($status);
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode($data); echo json_encode($data);
exit; exit;
} }
protected function xml($data) {
header('Content-Type: application/xml; charset=utf-8');
echo $data;
exit;
}
protected function text($data) {
header('Content-Type: text/plain; charset=utf-8');
echo $data;
exit;
}
} }

View file

@ -21,8 +21,9 @@ class Router {
// 2. Methode prüfen (z.B. /user/edit -> Methode edit()) // 2. Methode prüfen (z.B. /user/edit -> Methode edit())
if (isset($url[1])) { if (isset($url[1])) {
if (method_exists($this->controller, $url[1])) { $candidateMethod = $url[1];
$this->method = $url[1]; if (is_callable([$this->controller, $candidateMethod]) && strpos($candidateMethod, '_') !== 0) {
$this->method = $candidateMethod;
unset($url[1]); unset($url[1]);
} }
} }

View file

@ -10,7 +10,7 @@ class Security {
// Token beim POST-Request prüfen // Token beim POST-Request prüfen
public static function checkCsrf($token) { public static function checkCsrf($token) {
if (!isset($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) { if (!isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], (string)$token)) {
throw new Exception("Sicherheits-Token ungültig. Bitte lade die Seite neu."); throw new Exception("Sicherheits-Token ungültig. Bitte lade die Seite neu.");
} }
return true; return true;

View file

@ -1,5 +1,7 @@
<?php <?php
// Version: 01.10.2025 14:15 (FIX created_at COLUMN) $current_project = 'plants';
require_once __DIR__ . '/../auth.php';
session_start(); session_start();
require_once 'db.php'; require_once 'db.php';
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';

View file

@ -1,5 +1,6 @@
<?php <?php
// Version: 24.09.2025 16:29 $current_project = 'plants';
require_once __DIR__ . '/../auth.php';
require_once 'db.php'; // Stellt die $pdo-Verbindung her require_once 'db.php'; // Stellt die $pdo-Verbindung her

View file

@ -1,82 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Benachrichtigungs-Test</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f0f0;
margin: 0;
text-align: center;
}
.container {
background-color: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
}
p {
color: #666;
margin-bottom: 20px;
}
button {
padding: 12px 24px;
font-size: 16px;
border: none;
border-radius: 5px;
background-color: #007bff;
color: white;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<h1>Test für Browser-Benachrichtigungen</h1>
<p>Klicke auf den Knopf, um eine Test-Benachrichtigung auszulösen.</p>
<button id="testButton">Benachrichtigung senden</button>
</div>
<script>
document.getElementById('testButton').addEventListener('click', async () => {
// Schritt 1: Überprüfen und Erlaubnis anfordern
if (!('Notification' in window)) {
alert('Dieser Browser unterstützt keine Benachrichtigungen.');
return;
}
const permission = await Notification.requestPermission();
if (permission === 'granted') {
// Schritt 2: Benachrichtigung erstellen
const notification = new Notification('Test-Benachrichtigung', {
body: 'Herzlichen Glückwunsch, Benachrichtigungen funktionieren!',
icon: 'https://placehold.co/64x64/007bff/FFFFFF?text=OK'
});
// Schritt 3: Event-Listener für den Klick hinzufügen
notification.addEventListener('click', () => {
window.open('https://google.com', '_blank');
});
console.log('Test-Benachrichtigung gesendet.');
} else {
alert('Benachrichtigungsberechtigung wurde verweigert.');
}
});
</script>
</body>
</html>

View file

@ -1,228 +0,0 @@
<!-- Version: 24.09.2025 22:10 -->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prompt-Vergleich</title>
<style>
body {
font-family: sans-serif;
margin: 2em;
line-height: 1.6;
background-color: #f4f4f9;
color: #333;
}
.container {
display: flex;
gap: 20px;
margin-top: 20px;
flex-wrap: wrap;
}
.panel {
flex: 1;
min-width: 300px;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
h1, h2 {
color: #5E8C61;
}
pre {
background-color: #eee;
padding: 15px;
border-radius: 6px;
white-space: pre-wrap;
word-wrap: break-word;
font-size: 0.9em;
}
.loading-bar {
width: 0;
height: 4px;
background-color: #5E8C61;
transition: width 0.4s ease;
margin-top: 10px;
}
.controls {
margin-bottom: 20px;
}
.controls input[type="file"] {
margin-right: 10px;
}
.controls button {
padding: 10px 15px;
background-color: #5E8C61;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>Prompt-Vergleich</h1>
<p>Lade ein Bild hoch und vergleiche die Ergebnisse mit dem alten und dem neuen Prompt.</p>
<div class="controls">
<input type="file" id="imageInput" accept="image/*">
<button id="runTestBtn">Test starten</button>
</div>
<div id="loadingStatus" style="display: none;">
<p>Analysiere mit beiden Prompts...</p>
<div class="loading-bar"></div>
</div>
<div class="container">
<div class="panel">
<h2>Alter Prompt</h2>
<div id="oldPromptResult"></div>
</div>
<div class="panel">
<h2>Finaler Profi-Prompt (V6)</h2>
<div id="newPromptResult"></div>
</div>
</div>
<script>
const imageInput = document.getElementById('imageInput');
const runTestBtn = document.getElementById('runTestBtn');
const loadingStatus = document.getElementById('loadingStatus');
const loadingBar = loadingStatus.querySelector('.loading-bar');
const oldPromptResult = document.getElementById('oldPromptResult');
const newPromptResult = document.getElementById('newPromptResult');
const PROXY_URL = 'api_proxy.php';
const oldPrompt = `
Du bist ein liebevoller Pflanzenexperte. Analysiere das Bild dieser Pflanze. Gib mir die folgenden Informationen als sauberen JSON-String zurück, ohne zusätzlichen Text oder Markdown.
- "name": Der gebräuchliche Name der Pflanze.
- "latin_name": Der botanische (lateinische) Name der Pflanze.
- "watering_interval_days": Das Gießintervall in Tagen (nur die Zahl).
- "fertilizing_interval_days": Das Düngeintervall in Tagen (nur die Zahl).
- "care_tips": Gib mir 3 kurze und liebevoll formulierte Pflegetipps.
WICHTIGE REGELN FÜR DIE FORMARTIERUNG DER TIPPS:
1. Jeder Tipp MUSS mit einem passenden Emoji und einem Titel beginnen (z.B. "☀️ Licht:").
2. Zwischen den einzelnen Tipps MUSS sich ein doppelter Zeilenumbruch (\\n\\n) befinden.
Falls du die Pflanze nicht sicher erkennen kannst, setze den Wert für "name" und "latin_name" auf "Unbekannt".
`;
const newPrompt = `
Du bist ein kritischer Botaniker. Deine Aufgabe ist es, die Pflanze auf dem Bild wissenschaftlich und so exakt wie möglich zu identifizieren.
Gib das Ergebnis als sauberen JSON-String zurück, ohne zusätzlichen Text oder Markdown.
- "name": Der gebräuchlichste Name der Pflanze (nicht zwingend der deutsche).
- "latin_name": Der botanische (lateinische) Name der Pflanze.
- "plant_family": Die Pflanzenfamilie (z.B. "Araceae").
- "plant_genus": Die Pflanzengattung (z.B. "Monstera").
- "confidence_score": Bewerte deine Sicherheit bei der Identifizierung KRITISCH und ehrlich auf einer Skala von 0 (geraten) bis 100 (absolut sicher). Sei nicht übermäßig selbstbewusst. Ein Score von 95+ sollte nur bei perfekter Bildqualität und eindeutigen Merkmalen vergeben werden.
- "identification_notes": Gib eine sehr kurze Notiz, welche Merkmale zur Identifizierung geführt haben (z.B. "Herzförmige Blätter mit silbernen Flecken").
- "watering_interval_days": Das Gießintervall in Tagen (nur die Zahl).
- "fertilizing_interval_days": Das Düngeintervall in Tagen (nur die Zahl).
- "care_tips": Gib mir 3 kurze Pflegetipps.
WICHTIGE REGELN FÜR DIE TIPPS:
1. Formuliere die Tipps charmant aus der Ich-Perspektive der Pflanze (z.B. "☀️ Licht: Stell mich an einen hellen Ort, aber ohne direkte Mittagssonne.").
2. Jeder Tipp MUSS mit einem passenden Emoji und einem Titel beginnen (z.B. "☀️ Licht:").
3. Zwischen den Tipps MUSS sich ein doppelter Zeilenumbruch (\\n\\n) befinden.
WICHTIG: Falls dein confidence_score unter 75 liegt, setze "name", "latin_name", "plant_family", "plant_genus" auf "Unbekannt" und gib bei "identification_notes" den Grund für die Unsicherheit an (z.B. "Geringe Zuversicht aufgrund von schlechter Bildqualität.").
`;
runTestBtn.addEventListener('click', async () => {
const file = imageInput.files[0];
if (!file) {
alert('Bitte wähle zuerst ein Bild aus.');
return;
}
oldPromptResult.innerHTML = '';
newPromptResult.innerHTML = '';
runTestBtn.disabled = true;
loadingStatus.style.display = 'block';
loadingBar.style.width = '0%';
try {
const base64Image = await fileToBase64(file);
const analyzeWithProgress = async (prompt, resultElement) => {
try {
const result = await analyzeImage(base64Image, prompt);
displayResult(resultElement, result);
} catch (error) {
displayResult(resultElement, { error: error.message });
} finally {
loadingBar.style.width = (parseInt(loadingBar.style.width) + 50) + '%';
}
};
await Promise.all([
analyzeWithProgress(oldPrompt, oldPromptResult),
analyzeWithProgress(newPrompt, newPromptResult)
]);
} catch (error) {
console.error('Test failed:', error);
alert('Fehler beim Test: ' + error.message);
} finally {
runTestBtn.disabled = false;
loadingStatus.style.display = 'none';
loadingBar.style.width = '0%';
}
});
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = error => reject(error);
});
}
async function analyzeImage(base64Image, prompt) {
const requestPayload = {
"contents": [
{
"parts": [
{ "text": prompt },
{ "inline_data": { "mime_type": "image/jpeg", "data": base64Image } }
]
}
]
};
const response = await fetch(PROXY_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payload: requestPayload })
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`API error! status: ${response.status}, body: ${errorBody}`);
}
return response.json();
}
function displayResult(element, result) {
if (result.error) {
element.innerHTML = `<p style="color:red;">API Fehler:<br><pre>${result.error}</pre></p>`;
return;
}
try {
const textResponse = result.candidates[0].content.parts[0].text;
const jsonString = textResponse.replace(/```json|```/g, '').trim();
const plantData = JSON.parse(jsonString);
element.innerHTML = `<pre>${JSON.stringify(plantData, null, 2)}</pre>`;
} catch (e) {
element.innerHTML = `<p style="color:red;">Fehler beim Parsen der Antwort. Rohe Antwort:<br><pre>${JSON.stringify(result, null, 2)}</pre></p>`;
}
}
</script>
</body>
</html>

View file

@ -1,22 +0,0 @@
<?php
// DATEI: ./test/index.php
// 1. Name des Projekts definieren (muss exakt wie im Admin-Dashboard heißen)
$current_project = 'test';
// 2. Zentralen Türsteher aus dem Root einbinden
require_once __DIR__ . '/../auth.php';
// Ab hier ist die Seite sicher!
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Test Projekt</title>
</head>
<body>
<h1>Zugriff gewährt!</h1>
<p>Willkommen im Test-Projekt, <?= htmlspecialchars($_SESSION['user_email']) ?>.</p>
</body>
</html>